Home Sweat Home

An elegant integration of a hodgepodge of custom and consumer hardware.

Public Chat
Similar projects worth following
What started with my dishwasher dying evolved into a new home automation library and infrastructure that makes it super quick to create and integrate new devices--including custom hardware.

I'll describe here everything that's automated in my home and show how they're all elegantly tied together, from dishwasher controller, thermostat, RO water valve, wifi temperature sensors for freezers and such (all custom hardware costing less than $100 combined) to zwave and Shelly devices, to radon sensors and geiger counter, to controlling the TV and stereo, to voice commands (hosted in-house), automated reminders and so on.

[And yes, I spelled it "Sweat" in the title on purpose. ;) ]

The core of the system on the software side is a (python) library that makes it easy to publish and find attributes by name and type.  If you have a widget with a couple of switches, a light, and a dial, you simply publish four values to the home-automation network: two read-only booleans (the switches), a read-write boolean (the light), and a read-only real (the dial).  By convention, these are named in a path hierarchy, like "widget/switch1", "widget/dial", and so on.

The library takes care of all the dynamical aspects.  When someone turns the dial, your (driver) code just calls dial.set_value(v), and that's it.  If nobody is currently listening to the dial, nothing happens.  If one or more tasks anywhere on the network are listening (whether in the same application, or on a different machine entirely), they are notified (once!) that the value has changed.  When and if they care to know, they ask for the latest value and get it.  This is a little slower in terms of latency (on a scale irrelevant to humans) but wildly more efficient than the usual approaches in terms of total bandwidth, flow management, and all that practical crud that usually makes this stuff a headache.  And again, all of this happens in the library--neither end has to worry about it.

If you wanted that widget to control your bedroom lamp brightness, the code would look like this--and you could run this code on any machine on the network, regardless of where the code managing the widget or lamp were running (but it could all be in the same process just as easily):

lamp = ha.find("master_bedroom/north_lamp")
dial = ha.find("widget/dial")
async for val in dial:
    await lamp.send_value(val)

That for loop runs forever, quietly waiting for the dial to be turned, and then sets the lamp accordingly.  If the dial generates five values in the time it takes the lamp to receive and acknowledge the value, four of them are never even sent over the network (because they went stale before anyone needed them!).  And to be clear, the "async for" automatically sets up a subscription to the dial's value stream in the background, and if you exit the for loop, the subscription is closed.

Note it doesn't matter here whether the lamp or dial are on zwave, Shelly, custom, or whatever.  The above works even if the lamp or dial don't even exist at the time the code is run: the async for will wait for the dial to come online.  If you smashed the (zwave, say) widget and replaced it with a new custom device with new code on the driver side, the above loop doesn't even need to be restarted -- it will simply wait while you're changing the devices, and resume when the (new) widget/dial comes online.

Of course the interface to the dynamic values is much richer than just async iterators, but it's all pretty straightforward and obvious like that.

It would be very easy to add a nice GUI layer for a code-free experience, such as being able to simply visually connect  the "widget/dial" port to the "master_bedroom/north_lamp" port via a "wire" or whatever.  And that could run as a separate process with no need to modify any of the existing code (wouldn't even require restarting any processes besides the GUI itself).  But so far it's been so easy to code anything I've wanted I haven't needed that personally.

I did write a "monitor" which is like a hierarchical file browser for the entire HA network.  It's less than 1000 lines of python code and provides a curses-based interface which updates in real time as well as letting you set/change any settable values, graph historical plots, and so on.  This is a totally generic browser, non-specific to my particular network or devices.  Here's a snapshot of mine right now with a few things randomly opened (the ">" on the left show where the hierarchy can be opened).  Note this is just an administrative tool, not really intended to be a primary UI,...

Read more »

  • Getting our LG TV's Picture settings onto the HA network

    Simon05/03/2024 at 07:10 0 comments

    Our (LG, WebOS) TV was too bright at night (we keep our lights down, and mostly amber, past dusk..) but the settings were too buried to make it practical to tweak routinely by hand so I took another look at adding that to the network.   The only refs I found were for luna style urls, which I gather are normally only accessible by other apps running on the WebOS device itself (though not really sure about that).  But people on reddit were saying HomeBridge could do it, so I looked at their source and they were essentially hacking (in the exploit sense) the TV with a gross work-around.  But I'm not above doing whatever works, so I ran a couple of experiments and got it working using the same hack.  Surprisingly it appears to be 100% reliable so far, so much so that I was subsequently able to set it up so the brightness and contrast are continually adjusted to the time of day (whenever the TV is on; and the only really active period is a couple hours' ramp during dusk).

    The way it works is: there's an ssap url (the usual WebOS access, which was already set up and working) for popping up a dialog box, and you can set any url to hit when that dialog box is ok'd, canceled, or (critically) closed.  So you just set all three to the luna url, and then automatically close the dialog box (with another ssap call) as soon as its open, and that triggers a hit to the luna url from within the WebOS device itself.  (!!!Barf!!!)  On my TV you never even see the dialog box, so for all intents and purposes...  Well, the picture settings are now just another dynamic variable in the HA hierarchy along side the other TV properties like the volume, so it was very easy to script up the dusk ramp.  (Fwiw, I found it best to lower the contrast, which brings down the bright end, and to raise the brightness in order to rescue the darks, which otherwise get too dark to see.  That combination has the effect of darkening the picture while keeping everything still clear and visible.  I may just wrap that up as a "brightness" variable which you can just set with a real value 0-1 so that any calling code doesn't need to know these nuances.  But for the moment just the raw brightness, color, and contrast are directly exposed.)

    This is essentially the entire change to the low-level driver, and there's a bit more glue code in the next level up to publish this as an ordinary dynamic value on the HA net (note that these values can be polled via a normal ssap call so were already available read-only):

    async def set_picture(self, settings={'brightness': 50, 'color': 50, 'contrast': 95}):
        await self.send_luna_command('luna://com.webos.settingsservice/setSystemSettings',
                    'category': "picture",
                    'settings': settings
        await sleep(0.2)        # Give it a moment to exec the command?
        self.poll('picture')    # Poll the picture settings; hopefully they'll be updated by now.  This won't track manual changes!
    async def send_luna_command(self, uri, payload):
        """This hack issues a luna style command by setting it as the on-close of a dialog
            which we open and then immediately close.  BARF.  Borrowing this hack (conceptually)
            from the HomeBridge webos driver.
        luna_call = { 
            'uri': uri,
            'params': payload,
        buttons = [{
              'label': 'Ok',
              'focus': True,
              'buttonType': 'ok',
              'onClick': uri,
              'params': payload,
        payload = { 
                "title": 'Picture Settings',
                "message": 'Updating...',
                "modal": True,
                "buttons": buttons,
                "onclose": luna_call,
                "onfail": luna_call,
                "type": 'confirm',
                "isSysReq": True,
        # Open the pop-up:
        rv = await self.do_command("ssap://system.notifications/createAlert", payload)
        # rv e.g.:
        #  'payload': {'alertId': 'com.webos.service.apiadapter-1714593888911',
        #              'returnValue': True},
        # Then close it!
        if (alert_id := rv.get('alertId')) is None:
            self.log("WARNING: reply has no alertId.")
        payload = { 'alertId': alert_id }
        await self.do_command('ssap://system.notifications/closeAlert'...
    Read more »

View project log

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates