Skip to content

entities platforms (sensor, binary_sensor, switch) #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from

Conversation

dlashua
Copy link
Contributor

@dlashua dlashua commented Dec 28, 2020

Implement Native Home Assistant Entities (through the Entity Registry) in Pyscript.

In comparison to a simple state.set('domain.entity', value) it offers several features:

  1. visualize all entities in the pyscript integration
  2. renaming entity_ids
  3. grouping entities into devices (device management will be a future PR)
  4. handling service calls against entities (i.e. switch.turn_on for a switch)

If you'd prefer to not have this in pyscript, I can build it as a separate custom_component designed to integrate well with pyscript (though useful without it as well).

Needed:

  1. variable name to attach entity manager to in pyscript (using em during testing)
  2. using the entity manager directly in a pyscript (i.e. not inside of a function) doesn't always work because sometimes the pyscript loads before the platform loads. Looking to find a way to delay pyscript loading until after platforms have loaded, or, for a way to properly handle waiting for the platform to load if it's requested before it's ready. All attempts so far have left Home Assistant locked until the entity manager times out. (see EntityManager.wait_for_platform_register())

Missing:

  1. DOCS
  2. TESTS

@dlashua
Copy link
Contributor Author

dlashua commented Dec 28, 2020

Syntax for sensor and binary_sensor:

@time_trigger('startup')
def startup():
    # Make a Sensor
    s001 = em('sensor', 's001')
    s001.set('blah 0')

    # Make a Binary Sensor
    bs001 = em('binary_sensor', "bs001")
    bs001.set("on")

    s001.set('blah 1')

    task.sleep(5)

    s001.set('blah 2')

    bs001.set('off')

@dlashua
Copy link
Contributor Author

dlashua commented Dec 28, 2020

Syntax for switch:

This is a very simple switch that just turns on when you ask it to turn on, and off when you ask it to turn off (basically, it's an input_boolean):

@time_trigger('startup')
def startup():
    # Make a Switch
    sw001 = em('switch', 'sw001')
    sw001.set("on")

    @sw001.on_turn_on
    def handle_turn_on(entity, **kwargs):
        entity.set('on')
    
    @sw001.on_turn_off
    def handle_turn_off(entity, **kwargs):
        entity.set('off')

ast_ctx is being passed around to handle the service calls. maybe there is a better way?

@dlashua
Copy link
Contributor Author

dlashua commented Dec 29, 2020

The following pyscript does 3 things:

  1. creates a sensor entirely managed by pyscript (seconds alive)

  2. creates a binary_sensor based on this first sensor

  3. creates a switch to turn on and off the functionality of the first sensor

started_tasks = []
registered_triggers = []

@time_trigger('shutdown')
def shutdown():
    for one_task in started_tasks:
        one_task.cancel()


# Seconds Alive Sensor
#######################
@time_trigger('startup')
def seconds_alive_startup():
    seconds_alive = em('sensor', 'sensor_seconds_alive')
    seconds_alive.set_name('Seconds Alive')
    seconds_alive.set_unit('secs')

def seconds_alive_runner():
    log.info('Seconds Alive Handler Starting')
    seconds_alive = em('sensor', 'sensor_seconds_alive')
    cnt = 0
    while True:
        task.sleep(1)
        cnt += 1
        seconds_alive.set(cnt)

# Seconds Alive Even Sensor
#######################
@time_trigger('startup')
def sa_even_startup():
    sa_even = em('binary_sensor', 'seconds_alive')
    sa_even.set_name('Seconds Alive Even')
    sa_even.set_device_class('opening')

    sa_entity_id = 'sensor.seconds_alive'

    @state_trigger(sa_entity_id)
    def sa_even_handler():
        try:
            if int(state.get(sa_entity_id)) % 2 == 0:
                sa_even.set('on')
            else:
                sa_even.set('off')
        except:
            pass

    registered_triggers.append(sa_even_handler)


# Second Alive Switch
############################
@time_trigger('startup')
def sa_switch_startup():
    sa_switch = em('switch', 'switch_seconds_alive')
    sa_switch.set_name('Seconds Alive Active')

    sa_switch.set('off')

    sa_task = None

    @sa_switch.on_turn_on
    def start_seconds_alive(entity, **kwargs):
        nonlocal sa_task
        sa_task = task.create(seconds_alive_runner)
        entity.set('on')
        started_tasks.append(sa_task)
    # sa_switch.on_turn_on(start_seconds_alive)

    @sa_switch.on_turn_off
    def stop_seconds_alive(entity, **kwargs):
        nonlocal sa_task
        sa_task.cancel()
        entity.set('off')
    # sa_switch.on_turn_off(stop_seconds_alive)

@dlashua dlashua changed the title WIP: entities platforms entities platforms (sensor, binary_sensor, switch) Dec 29, 2020
@dlashua
Copy link
Contributor Author

dlashua commented Dec 30, 2020

On Device Management...

Adding an entity to a device is quite simple. However, if that entity goes away, the device still stays in Home Assistant with no easy way to remove it. Device cleanup is already cumbersome in Home Assistant. Add to that that we don't know which devices are still in use until after all pyscripts have been evaluated and it gets really hard.

We either need to grab a list of devices from Home Assistant, wait some time after startup for all entities to register, and then iterate over the device list and remove those no longer in use or, we need to be more explicit about which devices and entities we create (some kind of manifest alongside the pyscript that gets read first and handled before the pyscript loads).

So, I'm going to hold off on handling devices for now. I'll add some attributes to the entity so that, even if a user changes the entity id, they'll always be able to find the pyscript that is creating it.

@craigbarratt
Copy link
Member

Just starting to look at this. This is definitely outside my understanding of HASS, so I need to learn some more before I can offer useful input.

Does this address #120 too?

@dlashua
Copy link
Contributor Author

dlashua commented Dec 31, 2020

It does not address #120 directly. But it provides the features pyscript would need to address it (specifically the "switch" platform).

To fully support #120, we'd then need a decorator (@automation_switch('thing_here') for instance) that would first do em.get('switch', 'automation_thing_here'). And then have the on_turn_on and on_turn_off set something in TrigInfo to tell the Trigger not to run when it's turned off.

With this PR, though, someone could implement automation_switch manually. Something like:

automation_switch = em.get('switch','my_automation')
@automation_switch.on_turn_on
def automation_on(entity, **kwargs):
    entity.set('on')

@automation_switch.on_turn_off
def automation_off(entity, **kwargs):
    # Kill it if it's running already
    task.unique('doit')

    entity.set('off')

@state_trigger('input_boolean.test_1 == "on"')
@task_unique('doit')
def doit():
    if automation_switch.state == "off":
        log.info('not doing it')
        return

    #alternate implementation to show entity_ids at work
    automation_switch_entity_id = automation_switch.entity_id
    if state.get(automation_switch_entity_id) == "off":
        log.info('not doing it')
        return
    
    log.info('doing it')
    task.sleep(15)
    log.info('i did it')

Outside of making switches, or anything that can have a service_call made against it (since they simply don't work correctly with state.set()) there are pros and cons to using this compared to plain old state.set.

The biggest Pro is that the entity_id is now managed by the user. If I make a pyscript App that introduces a new weather information source, for example, and I use state.set, I have to pick an entity_id to set -- weather.daniel_weather_source. But that's probably not what the user wants to refer to it as. If I write my pyscript app well, I'll give the user a config option to set the entity_id to whatever they want, but that means they have to edit the App YAML to change it. If I use EntityManager to set the state instead, they could change the entity_id from the Home Assistant UI. Home Assistant also protects the entity_id. Without EntityManager, I could have several pyscript apps all writing to weather.daniel_weather_source ... or even a weather entity not created by pyscript ... and Home Assistant will happily overwrite the state data in that entity. But, with EntityManager the value I pass (em.get('sensor', 'this_value_here')) is a unique ID within Home Assistant, and not the entity_id.

The biggest Con to using this method, outside of jumping through the hoops to do it correctly, is that you don't know what the entity_id of something is unless you ask for it, and it can change. So, if your pyscript app creates an entity that is also then used by your pyscript app somewhere else, you're going to have some trouble and more hoops to jump through. With state.set, you know what the entity id is because your script (or it's YAML config) decided what that entity_id would be, not Home Assistant and its UI. This isn't generally an issue because the app can use some kind of internal state to track whatever it needs to track, but I have run into the issue on one app that I've written.

This whole situation has to do with the way Home Assistant was designed and, if starting from scratch, I think I'd do it differently, but, I digress.

For sensor, binary_sensor, and anything else that just provides data (i.e. cannot be interacted with), in reality, there's not enough Pros to outweigh the Cons and this may as well not exist.

But, when it comes to entities that can have a service call against them (switch.turn_on(entity_id='switch.daniel')), Home Assistant refuses to carry the service call on the event bus if the switch was not registered using the method EntityManager uses. This is another design choice they made. The Pro is, if you typo a switch entity_id in a service call, you get an error. The Con is, you have to jump through Home Assistant hoops to register the entity.

The code is simple, once the pattern is understood: the objects have to expose certain methods that Home Assistant uses to do what it does (you can see these objects and methods in sensor.py, switch.py, and binary_sensor.py as well as in the base class in entity_manager.py), the object has to be registered using async_add_entities from the platform, and each platform (switch, sensor, etc) needs a startup function in a properly named file within the component (these things are done at the top of the platform files, as well as a few lines in __init__.py to tell Home Assistant we want this to be done). Everything else is just trying to make it easy to use for the pyscript user.

It should also be noted that Entities made this way CAN indicate their entity_id (and not use a unique id at all). Home Assistant will automatically append "_2", "_3", "_x" as needed if the entity_id already exists. This is the "old way" that everything in Home Assistant used to work, and it's still supported. But I figure, if we're going to do it, we may as well do it in the currently recommended way. Though, Native Automations even, that don't have an "id" set in them, work this way. So it's still used by the core in some places.

@dlashua
Copy link
Contributor Author

dlashua commented Dec 31, 2020

Before you understand the HASS pieces of this, you can still provide input on the Python API for this. Right now, I've done it "simple", but it's not very "pyscript".

It has one entry point: em (which needs to be renamed... I'm thinking entity but I'm worried that people already use that as a variable, I know I do). It's used like this (and I tried to mimic pyscript's state as closely as I could):

entity = em(platform, unique_id)
entity.set(state, attribute1="a", attribute2="b", new_attributes={})

log.info(f"the state is {entity.state}")

To make it more pyscript the syntax could look like this:

em.platform.unique_id = state
em.platform.unique_id.attribute1 = "a"
em.platform.unique_id.attribute2 = "b"

log.info(f"the state is {em.platform.unique_id}')

# but also
em.set(platform, unique_id, state, attribute1="a", attribute2="b", new_attributes={})

log.info(f"the state is {em.get(platform, unique_id)}")

I am intentionally avoiding using platform.unique_id (i.e. switch.unique_id) as an identifier because that looks like an entity_id and this is NOT an entity_id. Essentially, em.platform.unique_id could just proxy the method calls to the actual object and could present itself as a string, but, using the current pattern is a lot easier to write (in the pyscript core) and perhaps having a different use pattern is a good thing because it'll help reinforce that this is not an entity_id.

Also, this API pattern breaks the ability to use @entity.on_turn_on as a decorator to define the turn_on method of a switch since this isn't valid python (I think you can only have one "." or something):

@em.switch.my_switch.on_turn_on
def blah():
  pass

For instance, you can't do a @state_trigger on one of these (@state_trigger('em.platform.unique_id == "test"')), since state_changed events only happen on entity_ids, not on the unique_ids inside the entity. Of course, with the right amount of magic, even this could work. state_trigger would have to monitor the Entity object for changes to self.entity_id and then reconfigure the trigger when the entity_id changed. Though, I think maybe there's some home assistant way to listen for changes to entities defined like this as long as they also have a device (ours do not for above mentioned reasons), because Native Home Assistant Automations can be written against the device (instead of the entity). I don't do this at all, myself, because everything works against machine generated device_ids (which are painful to read and write), though it works well if you use the UI based Automation Editor (I don't).

@raman325
Copy link
Contributor

raman325 commented Jan 1, 2021

This is all a bit confusing to me (speaking as someone who does have a decent understanding of how entities work) so I may be overstepping or not understanding correctly, but it seems like the problem you are trying to solve here is to be able to create an entity without all of the boilerplate code that HA requires in order for you to create a new integration. If I am understanding the problem correctly, the cleanest way to achieve this, IMO, is to:

  • have the user create a class that inherits from a platform entity (switch, sensor, etc.). Doesn't really matter what platform, the concept is more or less the same.
  • have the user create an instance of the class in some sort of setup function that gets passed to pyscript
  • have pyscript do all of the work to add the entity to HA, then from there provide a reference back to the entity that the user can use in code. This reference is not the entity_id, it's a direct reference to the object, and we can make it global to the app, which therefore eliminates the problem of entity_id's changing.

Thoughts?

@raman325
Copy link
Contributor

raman325 commented Jan 1, 2021

in re-reading what you wrote, I think your unique_id is what I am referring to so it sounds like you are trying to accomplish this already using the Entity Manager. The only thing I'd change here is that the entity classes being defined for each platform should inherit from the platform entity class (e.g. BinarySensorEntity) and PyscriptEntity (which I think should be renamed so that we can create a PyscriptEntity for #120) becomes a mixin. This allows users to define their own custom platform entity classes in addition to providing the helper functions to set the state if you want the entity to be automation driven (either fully or partially)

@dlashua
Copy link
Contributor Author

dlashua commented Jan 1, 2021

@raman325 extending the more specific HomeAssistant Entity classes and using PyscriptEntity as a mixin on those classes is a good idea. I can do that.

We could, instead, provide a register method on entity_manager so that a user would just write a class that extends an existing Entity Class and implement the logic that way. I think, for some users, this will end up being more complicated, though.

It also makes things a bit more difficult to manage. em.register will send that object in to Home Assistant. So, if you then update your custom class and reload, em.register will fail because that Entity is already registered. So, I'll have to add some code that determines if it's already registered and, if it is, removes it, and then readds it.

I will also, likely, have to translate between the native python class that Home Assistant expects, and the 'pyscript' class that user defined classes actually look like internally. It might just be easier to write classes for the user to extend, and have the classes we send to Home Assistant just proxy to that user defined class. That way, the class Home Assistant sees never changes.

So I guess it boils down to what is easier for the user. Do we make a "flask-style" decorator based interface like I have now? Or, do we made a class-based interface and proxy them through?

Here's a simple "virtual switch" (basically, an input_boolean) implemented in the "flask-style" way:

 my_switch = em.get('switch', 'my_virtual_switch')

@my_switch.on_turn_on
def turn_on(entity):
    entity.set('on')

@my_switch.on_turn_off
def turn_off(entity):
    entity.set('off')

And here's that same virtual switch implemented in what I imagine the end result will look like in a class-based way:

class VirtualSwitch(PyscriptSwitch):
    def __init__(self, unique_id):
        self._unique_id = unique_id

    def turn_on(self):
        self.set('on')

    def turn_off(self):
        self.set('off')

my_switch = VirtualSwitch('my_virtual_switch')
em.register(my_switch)

we could, of course, support both interfaces. It's just more work during initial code, documentation, support, and with future changes

thoughts?

@dlashua
Copy link
Contributor Author

dlashua commented Jan 2, 2021

I realize now, if we go the "class" route, we can save the user from having to deal with unique_id by providing a second stage init method they can override. Which makes the simple code above look like this:

class VirtualSwitch(PyscriptSwitch):
    def turn_on(self):
        self.set('on')

    def turn_off(self):
        self.set('off')

my_switch = VirtualSwitch('my_virtual_switch')
em.register(my_switch)

@dlashua
Copy link
Contributor Author

dlashua commented Jan 3, 2021

Investigating further on using, for instance, homeassistant.components.binary_sensor.BinarySensorEntity and our own PyscriptEntity as a mixin...

It can be done. However, I think the resulting code is more complicated and with no benefit. Take, for example, one small piece of the whole puzzle: the should_poll method. BinarySensorEntity inherits from Entity. Entity defines should_poll. However, in the case of pyscript, none of our entities should poll. Since the default Entity.should_poll method returns True, we need to over ride this. I can define it in the mixin PyscriptEntity, however, that goes against the way a mixin should work (since the parent class of PyscriptBinarySensorEntity would be BinarySensorEntity which inherits from Entity which already defines should_poll. So, in order to honor that, PyscriptBinarySensorEntity will need to override should_poll. And so will PyscriptSensorEntity, and PyscriptSwitchEntity.

There's nothing wrong with this, it's just more verbose than just extending PyscriptEntity which extends Entity. However, it's probably the "right thing to do", so I'll proceed. But, if you see the code and wonder why it's needlessly complex, this is why. :)

@dlashua
Copy link
Contributor Author

dlashua commented Jan 3, 2021

As a Mixin, it was just too verbose. However, after looking over many other Home Assistant components, many of them simply inherit from both classes (the Home Assistant Entity that inherits from Entity, and their own Entity which also inherits from Entity). So, I did the same. Far less repetitive code that way, and everything still works as it did before.

We still need to decide if we want a flask-like decorator interface, or a class-based interface inside of pyscript. And we need a variable name other than em. Then I'll add docs and tests (if I can figure them out) and be ready to add more entity types.

@dlashua
Copy link
Contributor Author

dlashua commented Jan 6, 2021

After a lot of thought on this, I think sticking with the decorator-style interface is the way to go. It's not as "native" feeling to seasoned Python users, but, it's more inline with the rest of pyscript, easier for beginners, and still fully usable in a class if you prefer to use classes. If it becomes cumbersome for advanced usage, we can always add a class-based interface later on.

While it's a little verbose, I think "entity_get" is the best variable name to bind this to in pyscript: It indicates exactly what it does, and is unlikely to conflict with existing variables.

@lmamakos
Copy link

I'm so pleased to have happened across this! I was going to drop an issue asking about something just like this.

I've done some work with pyscript lately, enabled by the MQTT triggers that were recently added. Just some code to look for some MQTT messages with temperature and humidity data in them, dropping duplicate messages and creating humidity, temperature and battery sensors for each device. This is working pretty well! I show an earlier version of my code here https://community.home-assistant.io/t/a-new-approach-for-xiaomi-ble-temperature-sensors-with-esphome-mqtt-and-pyscript/267321

What was lacking is the ability to persist the entities over a Home Assistant restart, and also since the entities don't have an underlying "device", I can't assign them to areas (I think?) which might be useful. Right now, I recreate the entities on restart, though not until the first message arrives since I don't have the previous value lying around anywhere.. in the mean time, the Lovelace UI might look a little ugly until the entities are re-created. Not a huge problem in a practical sense..

It seems like this work might open the door to solving these problems, and that would be great! Using pyscript to do the development of an integration without having to restart Home Assistant during development would be amazing!

FWIW, after having actually done my first "real" thing with pyscript, I think the decorator approach matches the feel and style of pyscript pretty well. It's compact and doesn't need you to drag in what looks like a bunch of boilerplate that the class-based approach feels like.

The ability to do iterative and interactive development with pyscript is really wonderful, especially using the Jupyter Notebook tool to do so. It's just about magical! The ability to do interactive ad-hoc inspection of the code is very powerful, and I don't think people realize how powerful until they've done it once.

The pyscript work is quite amazing and wonderful; thanks very much!

@dlashua
Copy link
Contributor Author

dlashua commented Jan 16, 2021

@lmamakos thank you for the feedback, it's greatly appreciated.

This PR has stalled a bit. For binary_sensors and sensors there are other approaches that work just as well (see below). What I really need this for is the switch platform. And, with @raman325 's potential efforts (also seemingly stalled) in regard to automation switches, I didn't want two overlapping pieces of code in pyscript that did basically the same thing.

Until this is ready, look into pyscript's state.persist(). You can only persist entities in the pyscript domain. However you, like most people, are wanting to create sensor and binary_sensor entities. What I do is use a single entity to handle state persistence (pyscript.mqtt_temperature_thing) with attributes for each sensor that the app handles. On startup, I consult the pyscript. entity for initial values. And at shutdown (@time_trigger('shutdown')) I store the state of each entity into the pyscript. entity, making it ready for the next startup.

@dlashua
Copy link
Contributor Author

dlashua commented Jan 16, 2021

Another thought: when state.persist was built, it seemed inappropriate to persist a state in a domain other than pyscript.. Now that I have a much deeper understanding of Home Assistant's Entity system, I don't see an issue with this. Perhaps state.persist() could be opened up to any entity (easy enough to do, just a few lines of code to take out) making what you're doing much much easier (just persist every state that you set and it'll handle the rest). @craigbarratt , thoughts?

@raman325
Copy link
Contributor

Yup I have not had much time to work on the automation switches:

  1. I have some concerns about maintainability of the implementation path I chose.
  2. I've been contributing to a couple of other components that I have a higher need for functionality from at the moment.

I am with you that two methods is not the right path forward. I am OK with deferring to your approach. I also retract my vote against decorators - to @lmamakos 's point, they provide a good mechanism to iterate on entity development. You could achieve the same via a class (only implement the methods you care about, the rest should raise NotImplementedError) but given that this pattern already exists it makes sense that it should continue to be a pattern available to users.

I still think there will be a need for a class based approach as well. The decorator approach is fine for one off entities, but if someone wants to publish a pyscript app as an alternative to a direct HA integration, they will need a way to dynamically create entities. You can achieve this via decorators and closures, but at some point you are doing a lot of work under the hood that is made significantly easier by just exposing a superclass and letting the user inherit from it and create instances of it. To the point about all of the boilerplate code, you could abstract a substantial amount of that away from most users, letting advanced users overload the built in logic where necessary.

@raman325
Copy link
Contributor

Yup I have not had much time to work on the automation switches:

  1. I have some concerns about maintainability of the implementation path I chose.
  2. I've been contributing to a couple of other components that I have a higher need for functionality from at the moment.

I am with you that two methods is not the right path forward. I am OK with deferring to your approach. I also retract my vote against decorators - to @lmamakos 's point, they provide a good mechanism to iterate on entity development. You could achieve the same via a class (only implement the methods you care about, the rest should raise NotImplementedError) but given that this pattern already exists it makes sense that it should continue to be a pattern available to users.

I still think there will be a need for a class based approach as well. The decorator approach is fine for one off entities, but if someone wants to publish a pyscript app as an alternative to a direct HA integration, they will need a way to dynamically create entities. You can achieve this via decorators and closures, but at some point you are doing a lot of work under the hood that is made significantly easier by just exposing a superclass and letting the user inherit from it and create instances of it. To the point about all of the boilerplate code, you could abstract a substantial amount of that away from most users, letting advanced users overload the built in logic where necessary.

Last point on the subject and then I will drop it. I don't think we should go for achieving both right off the bat. But just keep that in mind when you design the pyscript classes that inherit from the different entity types. No need to expose it today but maybe something to expose in the future so try to design accordingly.

@raman325
Copy link
Contributor

raman325 commented Jul 7, 2021

@dlashua what all is needed to take this over the finish line? I may have some time coming up and can help with closing some of the gaps if you would be open to contributions

@dlashua
Copy link
Contributor Author

dlashua commented Oct 31, 2021

@raman325 the last time I looked at it is was complete and working. The only outstanding issues were in regard to the design of the API which I was happy with, but wanted to be sure @craigbarratt and others who might use these features agreed with.

At this point, it might need some cleanup since the pyscript core has likely changed, though, github seems to think there are no conflicts.

@dlashua dlashua closed this Jan 19, 2022
@jrlacharnay
Copy link

Hi @dlashua, did you give up on this project? Is it possible to publish it as a pyscript app?

@dlashua
Copy link
Contributor Author

dlashua commented Feb 23, 2022

@jrlacharnay It can't be a pyscript app, as it requires Home Assistant Setup functionality for each platform (switch, sensor, binary_sensor, climate, etc).

It is still functionality I want to see in pyscript. The last I checked (over a year ago) it was working just fine. I'm not sure if the PR is still valid since pyscript has changed quite a bit since then. But you're welcome to look at the branch and make whatever changes are needed to get it in your local copy of pyscript. The only things left to do were to get agreement that that API I designed is what was desired, as there were several ways to go with it.

@bostonswe92
Copy link

This is a feature I would love to see. I am so happy to see other people think along the same lines as me.

My development skills are probably outside the scope of creating this sadly. But looking through the thread, and seeing what you are discussing, I would still like to give my 5 cents, Hopefully it can perhaps trigger some new ideas or intrest in this feature.

This is how i envision how this feature could look and work. I think it would fit well with the style of pyscript and how pyscript works because its a nice and clean approach with minimal code for the user, yet still very versitile and powerful. That is, if it could even be implemented this way.

Example of a "custom pyscript" binary sensor, mimicing the suns position in the sky:


@entity(domain = "binary_sensor", entity_id = "custom_pyscript_sensor_1", name = "sun_on_off", unit = None )

@state_trigger("sun.sun")          #Optional. See notes below

def custom_sensor_update(state)
     if (sun.sun == "above horizon"):
          state = "on"
     else
          state = "off"
     state.update(state)            #Maybe if its need? See note below

  • A create/update function decorator for your custom entity, just like how services are are handled. It would define the entity type, entity_id, a friendly_name and a unit if needed. See final thoughts for more.

  • Optional: If you add a state_trigger decorator infront of it as well it will listen for state changes and only update your entity when it triggers. Borrowing that functionality that is already in py_script, then you dont have to write any push functionallity. If there is no state_trigger decorator it will update the entity at intervals. Perhaps the poll vs push issue could be solved internally in the py_script intigration. In Home assistant all pyscript entities could always registered as a polling entity. But internally in the updating functionality it does nothing if the state_trigger is attached to the function aswell. A bit of a botchy and resource wasting solution but it could be a work around.

  • Similarily to "trigger clousers" the function could be placed inside another function, creating sort of a entity factory app/script if that is what you desire.

  • The decorated function would be required to have one variable, in this case called "state", ". "state" is the state varible of your entity passed to the function by pyscript when ever a poll or push request comes in to the intigration. It contains the previus values or defualt values if this is the initialisation of the entity.

  • Updating is done via the user just updating the given state variable in the function how they see fit. And, if its teachnically necesary, the state variable could be passed to a pyscript updater function when the user is done changing the values. That function then uses the internal HA API to update the state.

  • If the entity is a switch then all one would have to do is disable any poll updating, One could still use a state_trigger to let the user mimic another switch, but even if that decorator wasnt used, any trigger would just come as a push from the home assistant user. The entity function the user defines simply exicutes what happens when you trigger the switch. Internally in the intigration the "turn_on", turn_off" and "toggle" just updates the entity state themself and then make a call to the function, The function doesnt really care if you toggled or turned on/off the switch. It only defines what happens with what state it is, which it know from the state variable.

  • Finally, it could also be nice to maybe forcebly add some script/app specific ID-Attribute to the entities so one could see what app/script they come from. This is so you can filter and identify them when you work with them in dashboards ect

Doing it this way I think you would only have to create one "custom module" for the entire entity functionallity, one size fits all (except a little special with switches) where you as a user just choose type.

The creation and the updating of the entities themself you let pyscript handle internally just like any other intigration. Just defining the diffrent platforms and how they binds with the "entity module". In most cases wouldnt that just be calling the decorated function and passing along the current state for updating? Then handling the retriving of the updated values in one of the ways discussed above. The only thing the user would do is define the function for the updating/execution behavior.

Maybe this approach makes it more managable to implement and fits better with how intigrations works in home assistant? I am not sure.

One would have to create the "entity module" and a few surrounding function for binding the updating and intializing of the entities. And then the decorator, exposing the function to the internal workings of the intigration.

The entity type platforms could be done one at a time after that . No need to make it work for every single entity type all at once. I think one can get extremly far just having binary_sensor, sensor and switch working.

Some final thoughts tho. As said earlier, the internal workings of pyscript is I think beyond the scope of my abillity and understanding as a devveloper. With that said, even I can see how this implementation would raise some security concernes. It could for instance treathen the HA server stability, if say an "entity factory" style of app got stuck in an infinite loop and wouldnt stop producing entities. Functionallity like this could be dangerous for inexperience and new users. And even pros can make error like this. To medigate that I had some final ideas for security features, to make this approach more safe :

  • If possible, one could add another parameter to the decorator, limiting how many entities can be created by that function. Migth be hard to get it to work in factory style scenarios where the other parameters in the decorator keep changing. But I dont know how the decorators work under the hood, so i cant say one way or another. The alternativs below are probably better.

  • Add a setting to pyscript (like hass_is_global) where the user can enter how many entities pyscript can create in totalt. Validate the input to never be bigger then a big, but managable amount. If an new user forgets it, it default to a safe number. In the entity creation process it throws an error or does nothing if it hits that limit.

  • Whenever a script or app is deleted, changed, reloaded or pyscript is removed, pyscript has to have the functionallity to go through all previusly created entities from that instance and deletes them. So you dont get any orphaned entities floating around in home assistant.

  • As a last resort, if everything else fails and something gone horribly wrong via a bug ect, add a service call to pyscript as a failsafe where you can manually delete enteties, provided they orginiate from pycript, so one doesnt delete other intigrations entities

Anyway. Sorry for a wall of text. If you would like to ball some ideas back and forth I am all ears. I can try to contribute in what ways I can aswell, if anyone is interested in reopening this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants