Jump to content

v3.1.14 in production ; cmd & scene controllers


Recommended Posts

Posted

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

 

  • Thanks 1
Posted

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.

 

Posted
# 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,
    }

 

Posted

Hi @Diesel,

First, appreciate the thought you have put into this ; you have pushed me to think through this thing.  I will seek to understand by asking a few questions.  First, a bit of how I see the virtual plugin.

Virtual was originally written to plug a hole which would bridge scenes to programs.  This is the switch, dimmer/generic.  Then devices were added to bridge variables to programs & add a bit of conversion math which didn't originally exist in ISY.  Finally, the garage device was added to bridge SSE server/client to programs.

In each case it is meant as a bridge to signal and allow control or status for a program to act on it, but not to replace the programming tool in ISY.  In fact I think most plugin's are really meant to bridge and in some cases augment but not replace the basic node, program, variable tool set.

So as an engineer I am often cognisant of the addage "just because I can, does not mean I should".

With the stated vision of this plugin & would like to sort / sift your chart of features in these added features. Scenes are handed by the ISY as a passthru of commands, not as part of the node. So in the nodedefs.xml is set up to "accepts" commands and to "sends" commands.  So the command set for scenes is limited to ones which other devices can send or receive.  Other commands are for use in programs.

 

Physical ; functionName ; cmdName ; scene use case                 ; program

***currently handled: 

Tap on    ; setON              ; DON          ; scene receiver                   ; any status;then

Tap off    ; setOF              ; DOF           ; scene receiver                   ; any status;then

holdUP    ; setLevelUp     ; BRT           ; scene receiver                   ; any status;then

holdDN    ; setLevelDown; DIM           ; scene c/r cmd                   ; any status;then

---            ;setDim             ; ---             ; scene cmd                        ; status;then

***left to system

Fast On    ; left to system, could be added to goto 100

Fast Off    ; left to system, could be added to goto 0

***not in system

Basically the "storage" and coordination of all the functions of a multi-dimmer circuit.

*** could be added

Like the switch node, can (and will) add "sends" of DON, DOF, BRT, DIM, which are on the "accepts" list.  After, lets access FDON, FDOF and decide how we want them to act.

*** not sure you are going to get what you want

coordination of a set of dimmers beyond DON, DOF, BRT, DIM, FDON, FDOF.  This is best done through programs.  The reason is that the plugin is not aware of the scene which the node is in.  *IT COULD BE*, but a bucket of effort.  I don't know of any plugins which do that, and not sure its in the list of "should". 

 

Lastly, appreciate the discussion, let me know if I misread anything you are proposing or if other ideas come up (or you just disagree).  We have a fun sandbox here & the possibilities are endless.  *OTHERS* , feel free to join the discussion, this is not my plugin, it's ours!

 

 

Posted (edited)

Hi @sjenkins

I really appreciate you for all the effort you've put into enhancing Virtual, and even more-so for being open to discussion. 

The issue I'm trying to solve is Insteon's inherent disfunction with scenes. Native Insteon scenes were created by Insteon because they're the only reliable way to simultaneously control multiple devices. Unfortunately, IoX considers scenes to be stateless, but not really. If a device is a member of multiple scenes, all of those scenes are considered to be in the same state as that device, even if that scene was never called. A scene doesn't have a "truth indicator" that indicates whether a scene is on or off. So we end up having to create a spaghetti of programs to work around this disfunction.

As we move forward with Insteon to work with HomeKit, Matter, and other platforms, this disfunction becomes an even bigger issue.

A multi-way circuit is just an example of this disfunction. Insteon's native approach is to add the devices all into a scene to keep them in sync. It works as expected when controlling any of the physical devices, but not within UDM or any other platform unless you control the scene itself. What's missing? A Virtual device to act like the physical device(s) in that scene.    

In my mind, a virtual device would behave the same as a keypad button that acts as a scene controller. A keypad button always reflects the true state of it's scene. It turns the scene on when turned on, turns it off when turned off, brightens and dims the scene when pressed. 

That said, I appreciate your thoughtful response and I understand your “bridge, not replace Programs” philosophy. Giving some more thought on how to keep Virtual within that scope, but still achieve what I believe can be a game changer for everyone, I propose; 

Phase A (bridge-only): Extend the Virtual Dimmer’s accepts/sends set to include DFON/DFOF alongside the existing DON/DOF/BRT/DIM. The node will handle/emit those events so Programs can key off Fast On/Off in addition to Tap/Dim/Brighten. No scene awareness or coordination in the plugin.

Phase B (optional, default off): Add a single optional text param FORWARD_TO_NODE. When set, the dimmer simply relays whatever it sends (DON/DOF/BRT/DIM/DFON/DFOF) to that node via IoX REST. It remains a bridge/relay — still no scene membership knowledge or multi-dimmer logic inside the plugin. Users who prefer pure Programs can leave it blank and get the exact behavior you described.

This way Virtual stays true to its mission (bridge signals to Programs), while enabling a parity command set with physical dimmers and, optionally, a tiny relay convenience for those who want it.

Let me know your thoughts and whether you need me to elaborate further.   

 

   

Edited by Diesel
Posted

I agree that the plugin is mainly a bridge to programs, and that the usual IoX programming should be used for logic and performing actions. Any additional baked in functionality is ok as long as it doesn't interfere with that.

Thanks again for maintaining it!

Posted (edited)

After another espresso or 2....some more thoughts to ensure it doesn't impede anyone using it as a bridge to programs, and actually enhances program functionality in Phase A;

Phase A (enhancement)

Minimal, philosophy-pure changes that help everyone and don’t alter scope.

1)nodedefs.xml  — add sends on the existing Virtual Dimmer + include fast on/off

(If Virtual’s “dimmer/generic” already exists, extend that nodeDef; otherwise add a “VLDIM” nodeDef.)

<nodeDef id="VLDIM" nls="VLDIM" name="Virtual Dimmer" icon="LightDimmer">
  <sts>
    <st id="ST" editor="percent" uom="51" />
  </sts>

  <!-- What this node can ACCEPT from scenes/programs -->
  <accepts>
    <cmd id="DON"><p id="value" editor="percent" /></cmd>
    <cmd id="DOF"/>
    <cmd id="BRT"/>
    <cmd id="DIM"/>
    <cmd id="DFON"/>
    <cmd id="DFOF"/>
  </accepts>

  <!-- What this node can SEND (for Programs to “Control is … then …”) -->
  <sends>
    <cmd id="DON"/>
    <cmd id="DOF"/>
    <cmd id="BRT"/>
    <cmd id="DIM"/>
    <cmd id="DFON"/>
    <cmd id="DFOF"/>
  </sends>

  <!-- Optional program-only convenience -->
  <cmds>
    <cmd id="SETLVL"><p id="value" editor="percent" /></cmd>
    <cmd id="QUERY"/>
  </cmds>
</nodeDef>

If your schema uses cmds only and no explicit <accepts>/<sends>, we still add handlers for DFON/DFOF and ensure controller events fire so Programs can “Control … is Fast On/Off”.

2) Node class — just add DFON/DFOF handlers and emit sends

In the existing Virtual Dimmer node:

def cmd_DON(self, command):
    # update ST and fire “DON sent” (so Programs can trigger)
    self._set_local_level(self._value_from(command, default=100))
    self.reportCmd('DON')

def cmd_DOF(self, command):
    self._set_local_level(0)
    self.reportCmd('DOF')

def cmd_BRT(self, command):
    self._step(+self.step_pct)
    self.reportCmd('BRT')

def cmd_DIM(self, command):
    self._step(-self.step_pct)
    self.reportCmd('DIM')

# NEW: Fast On/Off (for parity with physical dimmer)
def cmd_DFON(self, command):
    self._set_local_level(100)
    self.reportCmd('DFON')

def cmd_DFOF(self, command):
    self._set_local_level(0)
    self.reportCmd('DFOF')

# SETLVL remains for programmatic level changes (program use)
def cmd_SETLVL(self, command):
    pct = self._value_from(command, default=100)
    self._set_local_level(pct)
    # optional: do not report a scene “send”; this is a program-level op

reportCmd('XXX') should be whatever the Virtual server currently uses to raise the “Control is …” events so Programs can key off Tap On / Tap Off / Fast On / Fast Off / Brighten / Dim.

Result: Users can now write Programs like:

If Control ‘Virtual Dimmer’ is Fast On → Then Set Scene ‘SceneXYZ’ On

If Control ‘Virtual Dimmer’ is Brighten → Then Network Resource “SceneBRT”

…without the plugin doing any scene logic itself.

Phase B (Optional - opt in): one-line “Relay” to a target node (still a bridge)

Add a single param to the Virtual Dimmer (e.g., FORWARD_TO_NODE). When set, the node mirrors the exact sent command to that node via IoX REST. This doesn't introduce scene logic; it’s just pass-through.

Add two drivers/params for visibility & control (optional):

GV0 (string) — the target node address (scene group or device), display only.

GV1 (bool) — “momentary” cosmetic reset (return ST to 0 after 0.4s), optional.

In each send path (DON/DOF/BRT/DIM/DFON/DFOF):

if self.forward_to:
    # For scenes: DON with an optional level argument is supported by IoX REST.
    # For devices: DON/DOF/BRT/DIM/DFON/DFOF are passed as-is.
    self._isy.cmd(self.forward_to, 'DON', level_0_to_255)  # only when DON has a % value
    # or
    self._isy.cmd(self.forward_to, 'DFON')  # etc.

This still keeps Virtual squarely in “bridge/relay” land:

Program patterns (what users can do with Phase A)

Single-scene control (ramp by scene presets):

- IF Control ‘Virtual Dimmer’ is switched On → Then Set Scene ‘X’ On

- IF Control ‘Virtual Dimmer’ is switched Off → Then Set Scene ‘X’ Off

Fast on/off:

- IF Control ‘Virtual Dimmer’ is switched Fast On → Then Set Scene ‘X’ On (or 100% via NR)

- IF Control ‘Virtual Dimmer’ is switched Fast Off → Then Set Scene ‘X’ Off

Brighten/Dim stepping:

- IF Control ‘Virtual Dimmer’ is Brighten → Then Network Resource calling /rest/nodes/<scene>/cmd/BRT

- IF Control ‘Virtual Dimmer’ is Dim → **Then … /DIM`

Set exact level (%):

- Use SETLVL to set ST (program semantics), then NR: /rest/nodes/<scene>/cmd/DON/<0–255>.

- IoX Programs typically don’t pass a % to a Scene “Set On” natively; NR handles it cleanly.

 

Edited by Diesel
Posted

So..... I do plan on doing a rewrite of this plugin mostly to bring the python up to date ; not for features.  I am doing this with my Hunter Douglas plug in and am really happy with the readability and maintenance.

But, I just did a few of the changes above with the criterium it should not be breaking for anyone.  We will need to validate this.  The only issue is they did require profile changes which can be a pita when you do an update.

I put it in the beta (non-production) store as 1.1.15

  1. changed ST to OL (on level)
  2. send is now DON, DOF, DFON, DFOF, BRT, DIM, OL (send level)
  3. accepts is now the same with OL setting the OL
  4. added a "memory",
    1. DON uses the memory
    2. set at 100 initially
    3. persistent over reboot of ISY, plugin
    4. BRT, DIM, modify the memory, but if DIM to zero, sets to 10 so ON is never zero
    5. OL mods the memory but if zero sets to 10
    6. DFON does not affect the memory just sets level to 100
    7. DOF, DFOF sets level to zero, mods the memory if level not 100
    8. all seem to work for the programs as control / status
  5. Have not tested scenes, but should work for control for DON, DOF, DFON, DFOF
  6. need to test on effects for scenes for BRT, DIM, OL

Let me know if this scratches some of what itches.  Then we can go from there.

(I will make a post stating this, let's continue the discussion there to keep versions clean)

 

Guest
This topic is now closed to further replies.

×
×
  • Create New...