sjenkins Posted yesterday at 08:57 PM Posted yesterday at 08:57 PM enjoy, let me know if you find any issues. I have only added dimmers / generic for dim up and done being active in control for programs. Scenes will take more, and a choice, if we want them to maybe return to the last on positions and trigger the scene, or 100% / 0%. I am reaching the end of re-writing my Hunter-Douglas plugin to a less ugly Python. My plan would be to come here next. VERSION = '3.1.14' """ 3.1.14 DONE commands for switches, generic, dimmer, garage 3.1.13 DONE prevent direct poll from re-running DONE add notice if comms check fails DONE clean-up & debug 1 Quote
Diesel Posted 8 hours ago Posted 8 hours ago Thanks for this! Ideally, a Virtual Dimmer would behave the same as a physical dimmer for consistency. Consider a potential common use case of a virtual controller of a multi-way circuit scene.Virtual dimmer controls and keeps the scene, and physical dimmers all in sync. Maybe something along these lines? User action (virtual dimmer) Exact behavior to emulate Commands to send (to scene) Notes Single tap ON(top paddle) Go to scene’s programmed On Levels with scene’s ramp DON (no level parameter) Mirrors what a physical dimmer sends when used as a scene controller. Single tap OFF(bottom paddle) Ramp responders to off per scene link DOF Double-tap ON Fast On (immediate to full) DFON Bypasses ramp to 100%. Double-tap OFF Fast Off (immediate off) DFOF Bypasses ramp. Press & hold ON (top hold) Start brightening until release BMAN (once), then periodic BRT until release, then SMAN Repeat rate ~300–400 ms? Press & hold OFF (bottom hold) Start dimming until release BMAN (once), then periodic DIM until release, then SMAN Same repeat rate. Set Level(slider/command with %) Jump scene to specific runtime level DON/<0–255> Useful for admin UI or programmatic ontrol. Quote
Diesel Posted 7 hours ago Posted 7 hours ago # nodes/virtual_dimmer.py import time import requests import udi_interface LOGGER = udi_interface.LOGGER class IsyRest: """Tiny helper to call IoX REST for scene commands.""" def __init__(self, base_url, user, password, verify_ssl=True, timeout=5): self.base = (base_url or '').rstrip('/') self.auth = (user, password) if user else None self.verify = verify_ssl self.timeout = timeout def cmd(self, node_or_scene_addr, command, *args): if not self.base: raise RuntimeError("ISY_BASE_URL not configured") url = f"{self.base}/rest/nodes/{node_or_scene_addr}/cmd/{command}" if args: url += "/" + "/".join(str(a) for a in args) r = requests.get(url, auth=self.auth, verify=self.verify, timeout=self.timeout) r.raise_for_status() return True class VirtualDimmer(udi_interface.Node): """ Virtual Dimmer that: - As a Responder: updates its own ST on DON/DOF/BRT/DIM/etc. - As a Controller: forwards DON/DOF/DFON/DFOF/BRT/DIM/BMAN/SMAN to a native IoX scene (group). """ id = 'VLDIM' # must match nodedefs.xml # Drivers: # ST : 0-100 % level (uom 51) # GV0 : scene/group address this node controls (display) # GV1 : momentary flag 0/1 (if 1, returns to 0% after send) drivers = [ {'driver': 'ST', 'value': 0, 'uom': 51}, {'driver': 'GV0', 'value': 0, 'uom': 56}, {'driver': 'GV1', 'value': 0, 'uom': 2}, ] def __init__(self, polyglot, primary, address, name, isy_rest: IsyRest, scene_addr: str = None, momentary: bool = False, step_pct: int = 3): super().__init__(polyglot, primary, address, name) self.isy = isy_rest self.scene_addr = scene_addr # e.g. 0001, 0012 (group id) self.momentary = 1 if momentary else 0 self.step_pct = max(1, min(20, int(step_pct))) # ---------- lifecycle ---------- def start(self): self.setDriver('GV1', self.momentary, report=True, force=True) if self.scene_addr: try: self.setDriver('GV0', int(self.scene_addr), report=True, force=True) except Exception: # not numeric; ignore—GV0 is display only pass self.reportDrivers() def query(self, command=None): self.reportDrivers() # ---------- helpers ---------- @staticmethod def pct_to_255(pct: int) -> int: pct = max(0, min(100, int(pct))) return round(pct * 255 / 100) def _set_local_level(self, pct: int): pct = max(0, min(100, int(pct))) self.setDriver('ST', pct, report=True, force=True) def _send_scene(self, cmd, *args): if not self.scene_addr: LOGGER.debug(f"{self.address}: no scene_addr configured; skip send {cmd} {args}") return try: self.isy.cmd(self.scene_addr, cmd, *args) except Exception as e: LOGGER.error(f"{self.address}: scene command failed: {cmd} {args} -> {e}") def _momentary_reset(self): if self.momentary: time.sleep(0.4) self._set_local_level(0) # ---------- command handlers ---------- # DON (optionally with level 0-100 from Admin Console) def cmd_DON(self, command): lvl = 100 if command and 'value' in command: try: lvl = int(command.get('value')) except Exception: lvl = 100 self._set_local_level(lvl) # responder behavior self._send_scene('DON', self.pct_to_255(lvl)) # controller behavior self._momentary_reset() def cmd_DOF(self, command): self._set_local_level(0) self._send_scene('DOF') self._momentary_reset() def cmd_DFON(self, command): self._set_local_level(100) self._send_scene('DFON') self._momentary_reset() def cmd_DFOF(self, command): self._set_local_level(0) self._send_scene('DFOF') self._momentary_reset() def cmd_BRT(self, command): new_lvl = min(100, int(self.getDriver('ST')) + self.step_pct) self._set_local_level(new_lvl) self._send_scene('BRT') def cmd_DIM(self, command): new_lvl = max(0, int(self.getDriver('ST')) - self.step_pct) self._set_local_level(new_lvl) self._send_scene('DIM') def cmd_BMAN(self, command): self._send_scene('BMAN') def cmd_SMAN(self, command): self._send_scene('SMAN') # Explicit “Set Level” from programs (0–100) def cmd_SETLVL(self, command): try: pct = int(command.get('value')) except Exception: pct = 100 self._set_local_level(pct) self._send_scene('DON', self.pct_to_255(pct)) self._momentary_reset() commands = { 'DON': cmd_DON, 'DOF': cmd_DOF, 'DFON': cmd_DFON, 'DFOF': cmd_DFOF, 'BRT': cmd_BRT, 'DIM': cmd_DIM, 'BMAN': cmd_BMAN, 'SMAN': cmd_SMAN, 'SETLVL': cmd_SETLVL, 'QUERY': query, } Quote
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.