Skip to content

ISY994 sensor improvements#10805

Merged
balloob merged 19 commits intohome-assistant:devfrom
OverloadUT:isy994_sensor_improvements
Dec 14, 2017
Merged

ISY994 sensor improvements#10805
balloob merged 19 commits intohome-assistant:devfrom
OverloadUT:isy994_sensor_improvements

Conversation

@OverloadUT
Copy link
Copy Markdown
Contributor

@OverloadUT OverloadUT commented Nov 26, 2017

Description:

Binary Sensor Changes

Today, using Insteon sensors via the ISY994 platform is a pretty big pain. Issues:

  1. The user has to name the sensors very specifically inside ISY994 based on the Hass config option.
  2. The user has to specify the device_class manually using customize:
  3. If the hardware sensor is configured to use unique nodes for on and off (which allows more powerful scene configuration in the Insteon ecosystem) then the sensor entities will not function whatsoever inside Hass, requiring the user to set up a "virtual entity" using the Programs feature in the ISY994 platform. While functional, these virtual entities update slower than native devices and require a lot of user configuration inside the ISY994 software.
  4. More recent Insteon hardware report a daily heartbeat for the purpose of determining when a battery is dead, and there is no way to see this inside Hass due to it being an entity that never changes status.

This overhaul eliminates all of those pain points, and make all Insteon ISY994 sensors work out of the box with the following improvements:

  1. The multiple ISY994 "nodes" used by a sensor no longer each get unique entities in Hass, but instead all attach to a single binary_sensor.
  2. The binary_sensor logic is now smart enough to handle BOTH ways that hardware can report changes: Either by the primary node changing status, OR by the primary and negative nodes reporting ON events.
  3. All Insteon ISY994 sensor classes are now automatically determined by looking at the ISY994 "type" attribute, which indicates the exact hardware in use.
  4. The timestamp of the last heartbeat is now stored as a sensor attribute, allowing for automations to trigger on a missed heartbeat. On boot-up, the attribute will be set to STATE_UNKNOWN if and only if a heartbeat node is actually present for the device. This means that older sensors without the heartbeat feature will not get erroneously identified as missing heartbeats (if the automation is written correctly - I will provide an example in the updated docs)!

Event Support

This PR also adds ISY994 Control event support: all Insteon "control" events will now emit as Hass events. This allows automations based on button presses, double-presses, long-holds, and others!

Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#4207

Breaking Change Info

ISY994 sensor devices previously showed up as switches by default, and now they will show up as binary_sensors. Any automations set up to trigger based on those old switch versions should be updated to the binary_sensor versions. Note: For now, this only applies to people using the 5.x beta versions of the ISY firmware.

Checklist:

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • New dependencies have been added to the REQUIREMENTS variable ([example][ex-requir]).
  • New dependencies are only imported inside functions that use them ([example][ex-import]).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.

This allows hass to react directly to Insteon button presses (on switches and remotes), including presses, double-presses, and long holds
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super().__init__(node)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this, since you return None anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops; artifacts from refactoring this in to its own method.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you should use STATE_UNKNOWN for this. None is probably best.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's important for there to be a difference between the attribute not existing (None) and the attribute currently being "Unknown" because we haven't seen a heartbeat since the last boot. If it was simply None until the first heartbeat, an automation can't tell if this device ever SHOULD give a heartbeat.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attr = super().device_state_attributes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super().__init__(node)

Comment thread homeassistant/components/isy994.py Outdated
Copy link
Copy Markdown
Member

@MartinHjelmare MartinHjelmare Nov 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably move this subscription to the coroutine async_added_to_hass since the callback on_update requires hass to be set on the entity. This will not be happen until the entity is added to home assistant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Good catch to eliminate a race condition. This issue was present before as well!

Comment thread homeassistant/components/isy994.py Outdated
Copy link
Copy Markdown
Member

@MartinHjelmare MartinHjelmare Nov 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably move this subscription to the coroutine async_added_to_hass since the callback on_control requires the entity_id. This will not be created until the entity is added to home assistant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know what, it didn't occur to me that this PR is actually branched off of PR #10664, which is where this code comes from. That being said, I'll make the change to the base PR and rebase this one to that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is now done even though GitHub is not recognizing this comment as outdated)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this used?

Copy link
Copy Markdown
Contributor Author

@OverloadUT OverloadUT Nov 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I had intended to store the child nodes in this array, but then I suppose I never actually ended up needing to. It feels weird not storing them, but since it would not be used in the current implementation I'll just remove this!

Edit: Oh right, I do store the subnodes - they are just stored as explicit properties since each subnode needs to be handled uniquely

@OverloadUT OverloadUT changed the title Isy994 sensor improvements ISY994 sensor improvements Nov 26, 2017
The event handler method requires `self.hass` to exist, which doesn't have a value until the async_added_to_hass method is called. Should eliminate a race condition.
We now smash all of the subnodes from the ISY994 in to one Hass binary_sensor, and automatically support both paradigms of state reporting that Insteon sensors can do. Sometimes a single node's state represents the sensor's state, other times two nodes are used and only "ON" events are sent from each. The logic between the two forunately do not conflict so we can support both without knowing which mode the device is in.

This also allows us to handle the heartbeat functionality that certain sensors have - we simply store the timestamp of the heartbeat as an attribute on the sensor device. It defaults to Unknown on bootup if and only if the device supports heartbeats, due to the presence of subnode 4.
Now we automatically know which sensors are moisture, motion, and openings! (We also reverse the moisture sensor state, because Insteon reports ON for dry on the primary node.)
The one material change here is that the event subscribers were moved to the `async_added_to_hass` method, as the handlers depend on things that only exist after the entity has been added.
When the ISY first boots up, if a battery-powered sensor has not reported in yet (due to heartbeat or a change in state), the state is unknown until it does.
Copy link
Copy Markdown
Member

@MartinHjelmare MartinHjelmare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should ask @balloob about the timestamp in the state attributes, and possible alternatives.

@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Subscribe to the node and subnode event emitters."""
super().async_added_to_hass()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a coroutine, so you should yield from it to schedule it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the help here - I am a little unclear on when to yield in a coroutine. Are you saying that I should yield from the super() call, but the other calls in this method are fine?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

def state(self):
"""Return the state of the binary sensor."""
if self._computed_state is None:
return STATE_UNKNOWN
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return None. The base entity class will handle unknown states.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I see that the async_update_ha_state will indeed do this, but I feel like if we know the state is unknown, this method return STATE_UNKNOWN to be explicit. That's what the Entity version of this method does, so the established paradigm is to fall back on STATE_UNKNOWN.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we should let the entity base class handle this.

try:
if self.device_class == 'moisture':
return not self._computed_state
except AttributeError:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What object attribute access could lead to attribute error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - artifact of a refactor

Fix coroutine await, remove unnecessary exception check, and return None when state is unknown
@OverloadUT
Copy link
Copy Markdown
Contributor Author

OverloadUT commented Nov 28, 2017

To summarize the question that needs @balloob's input:

Some battery-powered Insteon devices send a heartbeat signal every 24 hours as a mechanism to tell if a battery has died. Currently I have this implemented as:

  • A heartbeat attribute that gets updated to the current time (datetime.now().isoformat()) whenever the heartbeat signal is received.
  • If we know the device is heartbeat-capable but haven't seen one yet (on a Hass reboot) then the attribute gets set to STATE_UNKNOWN (allowing an automation to trigger if it stays UNKNOWN for >24 hours, for example)
  • If we know the device is not heartbeat-capable, the attribute does not exist in the first place

@MartinHjelmare pointed out that timestamps in attributes is frowned-upon, but I'm not immediately seeing another approach here that keeps the edge case handling. What do you think? Is there any heartbeat precedent in other components?

Edit: The answer is that we're going to split the heartbeat functionality in to a separate binary_sensor that represents a healthy battery. If we miss a heartbeat, it'll flip to false. I'll work on that change!

@pvizeli pvizeli changed the title ISY994 sensor improvements WIP: ISY994 sensor improvements Nov 28, 2017
@pvizeli
Copy link
Copy Markdown
Member

pvizeli commented Nov 28, 2017

I set it to WIP. You can remove it after you implement that binary and remove the timestamp from attributes 👍

Now all heartbeat-compatible sensors will have a separate `binary_sensor` device that represents the battery state (on = dead)
Copy link
Copy Markdown
Member

@MartinHjelmare MartinHjelmare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. Some minor questions/comments below.

def _detect_device_type(self) -> str:
try:
device_type = self._node.type
except AttributeError:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this AttributeError happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to protect against the self._node.type not being set. It comes from a third party API that itself can be populated by fourth party code, so just being defensive here!

try:
self._negative_node.controlEvents.subscribe(
self._negative_node_control_handler)
except AttributeError:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check if self._negative_node is not None instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the Python pattern of "ask for forgiveness" for no other reason than it seems like the Pythonic way. Happy to change it, and I'd love to hear if you have thoughts about when except should be used vs checking for None.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's a simple check that is safe and always works, I go with an if. If there are multiple checks needed or if those checks don't always work, or if cases are unpredictable, I go with try... except. But it all depends... 😄
Here the case looks contained to me since we initialize the attribute to None.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! Since we control that property in a closed environment, we should just check for values that we know it can be. I just pushed the change for these comments

"""Send a heartbeat to our heartbeat device, if we have one."""
try:
self._heartbeat_device.heartbeat()
except AttributeError:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check if self._heartbeat_device is not None instead?

Copy link
Copy Markdown
Contributor Author

@OverloadUT OverloadUT Dec 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Same answer as the line 115 comment)

PyISY will report unknown states as the number "-inf". This is implemented in the base ISY994 component, but subcomponents that override the `state` method needed some extra logic to handle it as well.
@OverloadUT
Copy link
Copy Markdown
Contributor Author

I merged in some changes from PR #10664 which this is branched off of

if self.is_unknown():
return None
else:
return super().state
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this be used? All isy device classes except binary sensor program seem to override the state property anyway. If this call would go to the Entity class, it would return STATE_UNKNOWN, which would lead to the same outcome as the above if self.is_unknown() case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fan and light isy devices don't implement their own state property, so this base-class code would apply to them. I could move the logic in to those last types, but I like that having it in the base class also serves as a signal to future implementers that the UNKNOWN state needs to be handled if you override it.

This actually doesn't end up going to the Entity class! I had to test it a bunch because super()'s behavior is new to me, but super will actually call the implementation in the other base class of the child object.

To use Fan as an example, it is defined as ISYFanDevice(isy.ISYDevice, FanEntity). So state will first get handled by ISYDevice.state, and when it calls super() it kicks it on over to FanEntity.state which handles the fan-specific version of state.

@OverloadUT
Copy link
Copy Markdown
Contributor Author

@MartinHjelmare when you are done reviewing this PR, I'd love if you could toss the same message on to #10664, which this PR is branched off of (so all of the code in that PR is in the code you've been reviewing here)

@OverloadUT OverloadUT changed the title WIP: ISY994 sensor improvements ISY994 sensor improvements Dec 8, 2017
@MartinHjelmare
Copy link
Copy Markdown
Member

If that's true what's the point of #10664?

@OverloadUT
Copy link
Copy Markdown
Contributor Author

Originally, #10664 was meant to be a small PR adding an important feature that things like this PR and others would need. But it took quite a while to get the new PyISY version all done and published, so in the meantime I worked on this PR as well.

I was trying to stick to the "smaller PRs" request, but this one depended on that other functionality so there was no choice but to branch from it. In a perfect world that other one would have been merged weeks ago :)

I suppose since you've been reviewing all of the code here, I could just close that other PR and roll both new features in to this one. I'll go with whatever is preferable on that front.

@MartinHjelmare
Copy link
Copy Markdown
Member

Yes, please close the other PR.

@OverloadUT
Copy link
Copy Markdown
Contributor Author

Done. Let me know when we're looking good here and I'll get the documentation written up! I wanted to wait until the implementation was locked-in :)

@OverloadUT OverloadUT changed the title ISY994 sensor improvements WIP ISY994 sensor improvements Dec 11, 2017
@OverloadUT
Copy link
Copy Markdown
Contributor Author

Re-adding WIP tag. I have discovered some issues with new devices I purchased for testing this change, and possibly with the latest ISY firmware 😭

Not all Insteon sensors use the same subnode IDs for the same things, so we need to use different logic depending on device type. Negative node and heartbeat support is now only used for leak sensors and open/close sensors.
@OverloadUT OverloadUT changed the title WIP ISY994 sensor improvements ISY994 sensor improvements Dec 12, 2017
@OverloadUT
Copy link
Copy Markdown
Contributor Author

Okay, should be ready for a final review again. I bought a motion sensor to validate these changes, and it turns out they work totally differently. I added a simple check to only apply the new logic in this PR to leak and door sensors.

I also improved initial state support so that doors with an unknown state on bootup will report as unknown until the sensor reports for the first time.


split_type = device_type.split('.')
for device_class, ids in ISY_DEVICE_TYPES.items():
if split_type[0] + '.' + split_type[1] in ids:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use new style string formatting.

'{}.{}'.format(split_type[0], split_type[1])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Member

@MartinHjelmare MartinHjelmare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@OverloadUT
Copy link
Copy Markdown
Contributor Author

Excellent; I'll write up the documentation now!

@MartinHjelmare
Copy link
Copy Markdown
Member

Add a link to a docs PR and I think we can merge this.

Meant to do this originally; writing documentation revealed that this requirement was missed!
@OverloadUT
Copy link
Copy Markdown
Contributor Author

Sorry for the one additional commit! In writing the documentation I realized that I never added the small bit of logic necessary for pre-5.x firmware to work, which was always my intention.

@OverloadUT
Copy link
Copy Markdown
Contributor Author

Description updated with a link to the documentation PR, and also added event support description here, which became part of this PR.

@balloob balloob merged commit 1c8b583 into home-assistant:dev Dec 14, 2017
akatrevorjay added a commit to akatrevorjay/home-assistant that referenced this pull request Dec 15, 2017
…into dev

* 'dev' of https://github.com/home-assistant/home-assistant:
  Disable html5 notify dependency (home-assistant#11135)
  ISY994 sensor improvements (home-assistant#10805)
  Allow using more than one keyboard remote (home-assistant#11061)
  set default utc offset to 0 (home-assistant#11114)
  Add problem device class (home-assistant#11130)
  Always consume the no_throttle keyword argument. (home-assistant#11126)
  Skip HASS emulated Hue bridges from detection. (home-assistant#11128)
  update pyripple (home-assistant#11122)
  Add media position properties (home-assistant#10076)
  Fixed typo in automation.py (home-assistant#11116)
@fabaff fabaff mentioned this pull request Dec 16, 2017
@OverloadUT OverloadUT deleted the isy994_sensor_improvements branch December 22, 2017 19:57
@home-assistant home-assistant locked and limited conversation to collaborators Mar 30, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants