-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
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
Add Interlogix Ultrasync Alarm System Integration #42549
Conversation
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.
Is support for YAML import really required on a new integration?
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.
I had a quick look at your PR, hopefully this review is helpful. Especially the part around entity creation and data retrieval was unclear for me, it looks you are not really utilizing the DataUpdateCoordinator. For which purpose are you sending events in HA?
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class AuthFailureException(IOError): |
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.
class AuthFailureException(IOError): | |
class AuthFailureException(Exception): |
A custom exeception class should inherit from Eception
, not from IOError
. On the next line you should also add pass
, I believe. See https://www.programiz.com/python-programming/user-defined-exception.
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.
Not a problem; done! 👍
) | ||
|
||
except AuthFailureException: | ||
errors["base"] = "cannot_connect" |
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.
As noted above, will this only be called when a connection is not possible or also when the credentials are not correct? (wrong pin). Currently you will always show 'Failed to connect.' (cannot_connect
), which is not super helpful to the user.
AuthFailureException
should be mapped to invalid_auth
(Invalid authentication.). Otherwise you should rename the exception.
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.
👍 Done
) -> Dict[str, Any]: | ||
"""Handle user flow.""" | ||
if self._async_current_entries(): | ||
return self.async_abort(reason="single_instance_allowed") |
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.
Can users only have one Interlogix Ultrasync Alarm System
? Perhaps it is better to set a unique id and check for duplicates.
await self.async_set_unique_id(user_input.get(CONF_HOST)) # or another unique id, like a serialnumber
self._abort_if_unique_id_configured()
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.
Thank you for sharing this snippit of code. I think i did this properly; maybe you can review this part and let me know?
|
||
except Exception: # pylint: disable=broad-except | ||
_LOGGER.exception("Unexpected exception") | ||
return self.async_abort(reason="unknown") |
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.
return self.async_abort(reason="unknown") | |
errors["base"] = "unknown" |
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.
👍 Done!
|
||
# validate by attempting to authenticate with our hub | ||
|
||
if not usync.login(): |
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.
Are you the author of ultrasync
package? It would be better to raise an error from the package and capture it here. Now all auth failures (no internet, wrong credentials, timeout) will all raise the same exception and thus the same error. This is not very helpful to the user.
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.
I am the author, yes. I agree it's not ideal; but 3 errors you identified that are all captured into one only play a role for the users initial hookup. Then it just works flawlessly from that point on.
There is no internet requirement here; the panel is usually in the persons local home network. So presuming the user is already accessing their panel internal on their network (via web browser), they're just entering the same 3 entries here too (host
, pin
and password
). Basic troubleshooting would be to simply try the same host information in a new browser tab.
Either way, it's still a good suggestion, but I'd have to re-factor a section of the code to handle this sort of thing. Is this required to at the start to just get support for the Ultrasync Panel into HA?
|
||
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: | ||
"""Handle options update.""" | ||
await hass.config_entries.async_reload(entry.entry_id) |
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.
Reloading the config entries should not be necessary, since you are using a coordinator. You could just set the new update interval directly on the coordinator.
if entry.options[CONF_UPDATE_INTERVAL]:
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
coordinator.update_interval = timedelta(seconds=entry.options[CONF_UPDATE_INTERVAL])
await coordinator.async_refresh()
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.
I put this code in place; i'll give it a whirl
host=config[CONF_HOST], | ||
) | ||
|
||
self._init = False |
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.
Is this used? I couldn't find the use-case, if not, please remove.
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.
👍 Done
"status" | ||
] | ||
|
||
self._init = 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.
Is this used? I couldn't find the use-case, if not, please remove.
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.
Good catch; removed 👍
|
||
# At least one sensor must be pre-created or Home Assistant will not | ||
# call any updates | ||
hass.data[DOMAIN][entry.entry_id][SENSORS]["area01_state"] = UltraSyncSensor( |
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.
Why is this 'dummy' sensor created? In my opinion, it would make more sense to pull all available areas first in init.py and create the sensors from there. Eventually they could all pull their latest data from the DataUpdateCoordinator.
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.
This was required through advice provided to me by @raman325 who was a fantastic resource on the Home Assistant Discord chat at the time. Without per-creating a dummy sensor to work with nothing ever gets detected after. It's like a ticket into the event loop. If i don't create one at the initialization, future changes never get triggered.
If i do this, everything works great afterwards.
@property | ||
def device_state_attributes(self): | ||
"""Return the state attributes of the sensor.""" | ||
return self.__attributes |
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.
What are the possible attributes? You need to specify a list/dict of possible attributes, so it is clear what will be added. This cannot be dynamic.
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 problem is that they truly are dynamic. The ultrasync is dynamically configured from the panel or it's internal website. I don't knjow what is enabled and disabled until i first connect to the panel (using the pin/password provided by the configuration).
Once i can connect, i can then poll for all of the zones and sensors. Different sensors have different attributes associated with them (such as a sensor like a smoke detector, motion sensors, and door sensors, etc). So I'm able to poll this and build the sensor list dynamically. It actually works really well; I've had lots of positive feedback using HACS in the time being since here.
There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. |
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 rebase on latest dev branch and make sure the build passes.
async def async_setup(hass: HomeAssistantType, config: dict) -> bool: | ||
"""Set up the UltraSync integration.""" | ||
hass.data.setdefault(DOMAIN, {}) | ||
|
||
return 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.
We don't need this anymore. We can move the code to async_setup_entry
.
async def async_setup(hass: HomeAssistantType, config: dict) -> bool: | |
"""Set up the UltraSync integration.""" | |
hass.data.setdefault(DOMAIN, {}) | |
return True |
|
||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: | ||
"""Set up UltraSync from a config entry.""" | ||
if not entry.options: |
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 not entry.options: | |
hass.data.setdefault(DOMAIN, {}) | |
if not entry.options: |
await coordinator.async_refresh() | ||
|
||
if not coordinator.last_update_success: | ||
raise ConfigEntryNotReady |
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.
await coordinator.async_refresh() | |
if not coordinator.last_update_success: | |
raise ConfigEntryNotReady | |
await coordinator.async_config_entry_first_refresh() |
if not coordinator.last_update_success: | ||
raise ConfigEntryNotReady | ||
|
||
undo_listener = entry.add_update_listener(_async_update_listener) |
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.
undo_listener = entry.add_update_listener(_async_update_listener) | |
entry.async_on_unload(entry.add_update_listener(_async_update_listener)) |
|
||
hass.data[DOMAIN][entry.entry_id] = { | ||
DATA_COORDINATOR: coordinator, | ||
DATA_UNDO_UPDATE_LISTENER: [undo_listener], |
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.
DATA_UNDO_UPDATE_LISTENER: [undo_listener], |
|
||
# The hub can sometimes take a very long time to respond; wait | ||
# 10 seconds before giving up | ||
async with timeout(10): |
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.
This doesn't make sense. We have a poll interval of 1 second and a timeout of 10 seconds? The poll interval must be longer than the maximum time it takes to complete the poll.
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 issue here is 9/10 the poll will return in under 1 second. So this is just more of a cautionary handling of the 1 off (so it breaks out) (let setting a SIGALRM if you will).
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 it can happen 1/10 my comment above still needs to be resolved.
def __setitem__(self, key, value): | ||
"""Set our sensor attributes.""" | ||
self.__attributes[key] = value |
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.
This is not a pattern that we want. It would be ok to add a callback in the entity and connect that via our dispatch helper to a signal.
Normally though we let the coordinator return the new data that is fetched and let the entities that subscribe to the coordinator access that data as needed.
detected_sensors.add(sensor_id) | ||
if sensor_id not in sensors: | ||
# hash our entry | ||
sensors[sensor_id] = UltraSyncSensor( |
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.
Don't store the entity. If we need to ask the entity to do something from outside the entity we use our dispatch helper.
data=ENTRY_CONFIG, | ||
options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, | ||
) | ||
flow = init_config_flow(hass) |
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 set up the integration instead.
options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, | ||
) | ||
flow = init_config_flow(hass) | ||
options_flow = flow.async_get_options_flow(entry) |
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 hass.config_entries.options.async_init
.
There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. |
There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. |
Because there hasn't been any activity on this PR for quite some time now, I've decided to close it for being stale. Feel free to re-open this PR when you are ready to pick up work on it again 👍 |
Proposed change
Interlogix provides security solutions. One of which is their Self Contained (ZeroWire) Hub:
This wraps the UltraSync Hub tool I've already wrote. It additionally follows along a HA community thread identified here
Type of change
Example entry for
configuration.yaml
:This component will create these sensors:
area1state
: Area 1 Statusarea2state
: Area 2 Statusarea3state
: Area 3 Statusarea4state
: Area 4 StatusGenerally, most alarm systems only set up 1 area. So
area01_state
will probably be the only sensor used. The others will only activate if the areas are detected to exist (and are setup on the Hub).This integration uses the control flow setup and can be added through the HA . The sensors automatically are automatically added when this is complete:
Note: You can only be logged into the ZeroWire hub with the same user once; a subsequent login with the same user logs out the other. Since this tool/software actively polls and maintains a login session to your Hub, it can prevent you from being able to log into at the same time elsewhere (via it's website). It is strongly recommended you create a second user account on your Hub dedicated to just this service.
There are 3 services associated with this integration you can call at any time:
away
: Set's your alarm system to the Away Scene/Modestay
: Set's your alarm system to the Stay Scene/Modedisarm
: Disarms your alarm.Additional information
home-assistant.io/15482home-assistant.io/15588Checklist
black --fast homeassistant tests
)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest
.requirements_all.txt
.Updated by running
python3 -m script.gen_requirements_all
..coveragerc
.The integration reached or maintains the following Integration Quality Scale:
To help with the load of incoming pull requests: