-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
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
Huge ISY994 platform cleanup, fixes support for 5.0.10 firmware #11243
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice clean up!
"""Representation of an ISY994 cover device.""" | ||
|
||
def __init__(self, node: object): | ||
"""Initialize the ISY994 cover device.""" | ||
isy.ISYDevice.__init__(self, node) | ||
ISYDevice.__init__(self, node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super().__init__(node)
"""Representation of an ISY994 fan device.""" | ||
|
||
def __init__(self, node) -> None: | ||
"""Initialize the ISY994 fan device.""" | ||
isy.ISYDevice.__init__(self, node) | ||
ISYDevice.__init__(self, node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
@@ -106,23 +102,18 @@ def __init__(self, name: str, node, actions) -> None: | |||
ISYFanDevice.__init__(self, node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
homeassistant/components/isy994.py
Outdated
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If hass
is an argument, it is usually the first argument.
homeassistant/components/isy994.py
Outdated
try: | ||
folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component] | ||
folder = programs[KEY_MY_PROGRAMS]['HA.' + domain] |
There was a problem hiding this comment.
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.
homeassistant/components/isy994.py
Outdated
getattr(ISY.climate, attr + '_units')) | ||
climate_attrs = dir(climate) | ||
weather_nodes = [WeatherNode(getattr(climate, attr), attr, | ||
getattr(climate, attr + '_units')) | ||
for attr in climate_attrs | ||
if attr + '_units' in climate_attrs] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
"""Representation of an ISY994 light devie.""" | ||
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update per above.
"""Representation of an ISY994 lock device.""" | ||
|
||
def __init__(self, node) -> None: | ||
"""Initialize the ISY994 lock device.""" | ||
isy.ISYDevice.__init__(self, node) | ||
ISYDevice.__init__(self, node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
@@ -319,8 +313,6 @@ def unit_of_measurement(self) -> str: | |||
class ISYWeatherDevice(isy.ISYDevice): | |||
"""Representation of an ISY994 weather device.""" | |||
|
|||
_domain = 'sensor' | |||
|
|||
def __init__(self, node) -> None: | |||
"""Initialize the ISY994 weather device.""" | |||
isy.ISYDevice.__init__(self, node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
"""Representation of an ISY994 switch device.""" | ||
|
||
def __init__(self, node) -> None: | ||
"""Initialize the ISY994 switch device.""" | ||
isy.ISYDevice.__init__(self, node) | ||
ISYDevice.__init__(self, node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
Good additional catches. All implemented! |
except (KeyError, AssertionError): | ||
pass | ||
status = program[KEY_STATUS] | ||
except (AttributeError, KeyError, AssertionError): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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! |
HIDDEN_STRING = hidden_identifier | ||
ignore_identifier = isy_config.get(CONF_IGNORE_STRING) | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These checks should be moved to a validator in the config schema.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could parse and insert the port in the config in the validator too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you can use vol.Url()
as the first validator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validator would be used in the config schema.
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema(vol.All({...}, validate_host))},
extra=vol.ALLOW_EXTRA)
homeassistant/components/isy994.py
Outdated
try: | ||
folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component] | ||
folder = programs[KEY_MY_PROGRAMS]['HA.{}'.format(domain)] | ||
except KeyError: | ||
pass | ||
else: | ||
for dtype, _, node_id in folder.children: | ||
if dtype is KEY_FOLDER: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String comparison should be done with ==
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--
homeassistant/components/isy994.py
Outdated
not have a type. | ||
""" | ||
try: | ||
device_type = node.type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead to make a exception and hard interrupt the flow of your program, just check if type will be a attribute.
homeassistant/components/isy994.py
Outdated
try: | ||
if node.node_def_id == 'BinaryAlarm': | ||
return True | ||
node_uom = set(node.uom) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
homeassistant/components/isy994.py
Outdated
try: | ||
device_type = node.type | ||
node_uom = set(map(str.lower, node.uom)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...
homeassistant/components/isy994.py
Outdated
|
||
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
always true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/dyson.py#L47-L48
If I've got that wrong I can remove the check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bootstrap setup a component only one time but the platform it could be call multiple times.
homeassistant/components/isy994.py
Outdated
if ISY994_NODES not in hass.data: | ||
hass.data[ISY994_NODES] = {} | ||
for domain in SUPPORTED_DOMAINS: | ||
if domain not in hass.data[ISY994_NODES]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
always true
homeassistant/components/isy994.py
Outdated
if domain not in hass.data[ISY994_NODES]: | ||
hass.data[ISY994_NODES][domain] = [] | ||
|
||
if ISY994_WEATHER not in hass.data: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
always true
homeassistant/components/isy994.py
Outdated
if ISY994_WEATHER not in hass.data: | ||
hass.data[ISY994_WEATHER] = [] | ||
|
||
if ISY994_PROGRAMS not in hass.data: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
always true
homeassistant/components/isy994.py
Outdated
if ISY994_PROGRAMS not in hass.data: | ||
hass.data[ISY994_PROGRAMS] = {} | ||
for domain in SUPPORTED_DOMAINS: | ||
if domain not in hass.data[ISY994_PROGRAMS]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
always true
Also removes two stray debug lines
Description:
Breaking Change Notes
hidden_string
feature 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_string
config 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.data
components/isy994.py
does. 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_string
feature. 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:
tox
run successfully. Your PR cannot be merged unless tests pass