Huge ISY994 platform cleanup, fixes support for 5.0.10 firmware#11243
Conversation
# * No more globals - store on hass.data # * Parent ISY994 component handles categorizing nodes in to Hass components, rather than each individual domain filtering all nodes themselves # * Remove hidden string, replace with ignore string. Hidden should be done via the customize block; ignore fully prevents the node from getting a Hass entity # * Removed a few unused methods in the ISYDevice class # * Cleaned up the hostname parsing # * Removed broken logic in the fan Program component. It was setting properties that have no setters # * Added the missing SUPPORTED_FEATURES to the fan component to indicate that it can set speed # * Added better error handling and a log warning when an ISY994 program entity fails to initialize # * Cleaned up a few instances of unecessarily complicated logic paths, and other cases of unnecessary logic that is already handled by base classes
| def __init__(self, node: object): | ||
| """Initialize the ISY994 cover device.""" | ||
| isy.ISYDevice.__init__(self, node) | ||
| ISYDevice.__init__(self, node) |
| def __init__(self, node) -> None: | ||
| """Initialize the ISY994 fan device.""" | ||
| isy.ISYDevice.__init__(self, node) | ||
| ISYDevice.__init__(self, node) |
| @@ -106,23 +102,18 @@ def __init__(self, name: str, node, actions) -> None: | |||
| ISYFanDevice.__init__(self, node) | |||
|
|
||
| SUPPORTED_DOMAINS = ['binary_sensor', 'cover', 'fan', 'light', 'lock', | ||
| 'sensor', 'switch'] | ||
| def _check_for_node_def(node, hass: HomeAssistant, |
There was a problem hiding this comment.
If hass is an argument, it is usually the first argument.
| for domain in SUPPORTED_DOMAINS: | ||
| try: | ||
| folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component] | ||
| folder = programs[KEY_MY_PROGRAMS]['HA.' + domain] |
There was a problem hiding this comment.
Use new style string formatting.
| weather_nodes = [WeatherNode(getattr(climate, attr), attr, | ||
| getattr(climate, attr + '_units')) | ||
| for attr in climate_attrs | ||
| if attr + '_units' in climate_attrs] |
| def __init__(self, node: object) -> None: | ||
| """Initialize the ISY994 light device.""" | ||
| isy.ISYDevice.__init__(self, node) | ||
| ISYDevice.__init__(self, node) |
There was a problem hiding this comment.
Please update per above.
| def __init__(self, node) -> None: | ||
| """Initialize the ISY994 lock device.""" | ||
| isy.ISYDevice.__init__(self, node) | ||
| ISYDevice.__init__(self, node) |
|
|
||
| def __init__(self, node) -> None: | ||
| """Initialize the ISY994 weather device.""" | ||
| isy.ISYDevice.__init__(self, node) |
| def __init__(self, node) -> None: | ||
| """Initialize the ISY994 switch device.""" | ||
| isy.ISYDevice.__init__(self, node) | ||
| ISYDevice.__init__(self, node) |
|
Good additional catches. All implemented! |
| except (KeyError, AssertionError): | ||
| pass | ||
| status = program[KEY_STATUS] | ||
| except (AttributeError, KeyError, AssertionError): |
There was a problem hiding this comment.
There doesn't seem to be an assertion check within this try...except.
Would it be possible to move all these try...except blocks for program out of the platforms and into the _categorize_programs function in the component? The aim would be that all programs in ISY994_PROGRAMS should be programs that should be added as entities. That would save a lot of near duplicated code. I see the block here in the binary sensor doesn't access the KEY_ACTIONS key on program. But that should be solvable in _categorize_programs by checking the domain.
There was a problem hiding this comment.
Yeah, I agree that's a much better way for this to work. I'll move it over.
Removes the need for a bunch of duplicate exception handling in each individual platform
Sensor platform was crashing when the ISY reported climate nodes. Logic has been fixed. Also added a config option to prevent climate sensors from getting imported from the ISY. Also replace the underscore from climate node names with spaces so they default to friendly names.
|
Alright, just pushed an update that moves all of the program building and validation to the component. Each platform is now so fresh and clean. I also pushed some more cleanup to the weather modules. Universal Devices was kind enough to activate some paid modules for me so that I could better test some of the stuff here, and weather was indeed broken after my refactor. I also added a config option to prevent importing climate sensors, as I'm not sure why a user would want to rely on the ISY for climate data when Hass has so many of its own ways to get weather. This allows a user to keep the climate modules turned on inside the ISY but not have to hide them all if they don't want them. I also made the weather nodes have spaces instead of underscores. It's awesome that this is not a breaking change because the entity_id ends up the same! |
| sensor_identifier = isy_config.get(CONF_SENSOR_STRING) | ||
| enable_climate = isy_config.get(CONF_ENABLE_CLIMATE) | ||
|
|
||
| if host.scheme == 'http': |
There was a problem hiding this comment.
These checks should be moved to a validator in the config schema.
There was a problem hiding this comment.
You could parse and insert the port in the config in the validator too.
There was a problem hiding this comment.
Maybe you can use vol.Url() as the first validator?
There was a problem hiding this comment.
Ah, I haven't actually looked in to the config schema framework. If it can handle the URL stuff that would be much better. I'll do some digging.
There was a problem hiding this comment.
@MartinHjelmare Do you have an example of what you're looking for? I looked in to this, and the config option is already being passes through cv.url which in turn uses vol.Url(). This code here isn't really doing validation, but just munging the URL in to the format that the PyISY library expects (all of this could/should be done inside PyISY if it just accepted a URL rather than a host, port, and https boolean, but it doesn't.)
I could remove the final else here, because cv.url already verified that we're either http or https, but I'm not sure anything more than that makes sense.
There was a problem hiding this comment.
def validate_host(config):
host = urlparse(config[CONF_HOST])
if host.scheme == 'http':
https = False
port = host.port or 80
elif host.scheme == 'https':
https = True
port = host.port or 443
config[CONF_HOST] = host
config[CONF_PORT] = port
config[CONF_HTTPS] = https
return configThere was a problem hiding this comment.
Ah, so this would be just moving it to a method that gets called from within setup, and injects in to the config Dict? It doesn't seem like this is something that happens inside the schema definition itself, right?
My concern with this is that it injects new config values in to the dict that didn't go through Voluptuous's Schema checks. A casual glance at the code (due to the new CONF consts) would also make it look like PORT and HTTPS can be provided as config options, even though they can't.
There was a problem hiding this comment.
The validator would be used in the config schema.
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema(vol.All({...}, validate_host))},
extra=vol.ALLOW_EXTRA)| pass | ||
| else: | ||
| for dtype, _, node_id in folder.children: | ||
| if dtype is KEY_FOLDER: |
There was a problem hiding this comment.
String comparison should be done with ==.
| not have a type. | ||
| """ | ||
| try: | ||
| device_type = node.type |
There was a problem hiding this comment.
instead to make a exception and hard interrupt the flow of your program, just check if type will be a attribute.
| try: | ||
| if node.node_def_id == 'BinaryAlarm': | ||
| return True | ||
| node_uom = set(node.uom) |
| """ | ||
| try: | ||
| device_type = node.type | ||
| node_uom = set(map(str.lower, node.uom)) |
|
|
||
| def setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||
| """Set up the ISY 994 platform.""" | ||
| if ISY994_NODES not in hass.data: |
There was a problem hiding this comment.
Doesn't setup get called multiple times if multiple isy994 blocks are specified in the config? I took this paradigm from here and I assumed that's why it was there:
If I've got that wrong I can remove the check
There was a problem hiding this comment.
The bootstrap setup a component only one time but the platform it could be call multiple times.
| if ISY994_NODES not in hass.data: | ||
| hass.data[ISY994_NODES] = {} | ||
| for domain in SUPPORTED_DOMAINS: | ||
| if domain not in hass.data[ISY994_NODES]: |
| if domain not in hass.data[ISY994_NODES]: | ||
| hass.data[ISY994_NODES][domain] = [] | ||
|
|
||
| if ISY994_WEATHER not in hass.data: |
| if ISY994_WEATHER not in hass.data: | ||
| hass.data[ISY994_WEATHER] = [] | ||
|
|
||
| if ISY994_PROGRAMS not in hass.data: |
| if ISY994_PROGRAMS not in hass.data: | ||
| hass.data[ISY994_PROGRAMS] = {} | ||
| for domain in SUPPORTED_DOMAINS: | ||
| if domain not in hass.data[ISY994_PROGRAMS]: |
Also removes two stray debug lines
Description:
Breaking Change Notes
hidden_stringfeature has been removed from the isy994 component. Previously, this allowed entities to be "hidden" in Home Assistant if a configured string was present in an ISY device's name or folder path. This was removed because hiding devices is now done via the customization feature.ignore_stringconfig option, which will now cause Home Assistant to completely ignore devices with the matching string so that they will not be imported as a Home Assistant device at all. This can be helpful if you have nodes in the ISY that aren't useful at all in Hass (IR transmitter nodes are a good example.)Initial Problem Statement
The 5.0.10 update to the ISY994 firmware completely broke Home Assistant's integration. They are deprecating the uom values as useful attributes, and developers are encouraged to use the NodeDefs instead.
Trying to add this support in Hass revealed how much the ISY994 component and platforms were in need of a refactor - many things about how it works would not pass a code review today. During the refactor I found many other issues that I fixed. So this ended up being a pretty large cleanup of all of the isy994 files.
List Of Changes
hass.datacomponents/isy994.pydoes. I organized it in a way that is hopefully easy to adjust in the future and commented as much as I could about what is going on. It is complex no matter how you shake it though, because we're supporting four entirely different ways of identifying device types, each that work with different families of devices and ISY firmware versions.hidden_stringfeature. Accomplishing this behavior can be done viacustomize, so it didn't make sense to me to have the logic in two different places. I REPLACED the feature withignore_string, which will now completely ignore a node that contains the string, preventing Hass entities from getting created in the first place. This is very useful to prevent a bunch of pointless nodes from getting Hass entities that just end up hidden.!! This change needs to be tested by other people with other versions of the ISY994 firmware and sets of devices. This is a very big change to the fundamental way that devices are set up! !!
Pull request in home-assistant.github.io with documentation (if applicable): (pending)
Example entry for
configuration.yaml(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
toxrun successfully. Your PR cannot be merged unless tests pass