From cf74f34d7e72eb6033d753d0c85adf14cee95cf6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 2 Feb 2020 17:24:38 -0700 Subject: [PATCH] Add support for real-time data from SimpliSafe --- .../components/simplisafe/__init__.py | 178 +++++++++++++-- .../simplisafe/alarm_control_panel.py | 206 ++++++++++-------- homeassistant/components/simplisafe/const.py | 25 ++- homeassistant/components/simplisafe/lock.py | 49 +++-- .../components/simplisafe/manifest.json | 2 +- 5 files changed, 337 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 7cd1fd1bb2d..e3d3fcba812 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -3,8 +3,9 @@ import asyncio import logging from simplipy import API -from simplipy.errors import InvalidCredentialsError, SimplipyError -from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF +from simplipy.entity import EntityTypes +from simplipy.errors import InvalidCredentialsError, SimplipyError, WebsocketError +from simplipy.websocket import get_event_type_from_payload import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -21,36 +22,49 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, ) +from homeassistant.util.dt import utc_from_timestamp from .config_flow import configured_instances -from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE +from .const import ( + ATTR_ALARM_DURATION, + ATTR_ALARM_VOLUME, + ATTR_CHIME_VOLUME, + ATTR_ENTRY_DELAY_AWAY, + ATTR_ENTRY_DELAY_HOME, + ATTR_EXIT_DELAY_AWAY, + ATTR_EXIT_DELAY_HOME, + ATTR_LAST_EVENT_INFO, + ATTR_LAST_EVENT_SENSOR_NAME, + ATTR_LAST_EVENT_SENSOR_TYPE, + ATTR_LAST_EVENT_TIMESTAMP, + ATTR_LAST_EVENT_TYPE, + ATTR_LIGHT, + ATTR_VOICE_PROMPT_VOLUME, + DATA_CLIENT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + VOLUMES, +) _LOGGER = logging.getLogger(__name__) CONF_ACCOUNTS = "accounts" DATA_LISTENER = "listener" +TOPIC_UPDATE = "update_{0}" + +DEFAULT_SOCKET_MIN_RETRY = 15 +DEFAULT_WATCHDOG_SECONDS = 5 * 60 -ATTR_ALARM_DURATION = "alarm_duration" -ATTR_ALARM_VOLUME = "alarm_volume" -ATTR_CHIME_VOLUME = "chime_volume" -ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" -ATTR_ENTRY_DELAY_HOME = "entry_delay_home" -ATTR_EXIT_DELAY_AWAY = "exit_delay_away" -ATTR_EXIT_DELAY_HOME = "exit_delay_home" -ATTR_LIGHT = "light" ATTR_PIN_LABEL = "label" ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" ATTR_PIN_VALUE = "pin" ATTR_SYSTEM_ID = "system_id" -ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" - -VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -117,6 +131,37 @@ def _async_save_refresh_token(hass, config_entry, token): ) +@callback +def async_create_event_from_raw_data(event_data): + """Create a generated payload from raw event data.""" + event_type = get_event_type_from_payload(event_data) + + # simplisafe-python will take care of logging a message if it finds an event + # type it doesn't know about, so if get_event_type_from_payload() returns + # None, just return: + if not event_type: + return + + try: + event_sensor_type = EntityTypes(event_data["sensorType"]).name + except ValueError: + _LOGGER.warning( + 'Encountered unknown entity type: %s ("%s"). Please report it at' + "https://github.com/home-assistant/home-assistant/issues.", + event_data["sensorType"], + event_data["sensorName"], + ) + event_sensor_type = None + + return { + ATTR_LAST_EVENT_INFO: event_data["info"], + ATTR_LAST_EVENT_SENSOR_NAME: event_data["sensorName"], + ATTR_LAST_EVENT_SENSOR_TYPE: event_sensor_type, + ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp(event_data["eventTimestamp"]), + ATTR_LAST_EVENT_TYPE: event_type, + } + + async def async_register_base_station(hass, system, config_entry_id): """Register a new bridge.""" device_registry = await dr.async_get_registry(hass) @@ -292,9 +337,27 @@ class SimpliSafe: self._config_entry = config_entry self._emergency_refresh_token_used = False self._hass = hass - self.last_event_data = {} + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + self._websocket_watchdog_listener = None + self.last_rest_api_data = {} + self.last_websocket_data = {} self.systems = None + hass.loop.create_task(self.async_websocket_connect()) + + async def _attempt_websocket_connect(self): + """Attempt to connect to the websocket (retrying later on fail).""" + try: + await self._api.websocket.async_connect() + except WebsocketError as err: + _LOGGER.error("Error with the websocket connection: %s", err) + self._websocket_reconnect_delay = min( + 2 * self._websocket_reconnect_delay, 480 + ) + async_call_later( + self._hass, self._websocket_reconnect_delay, self.ws_connect + ) + async def async_init(self): """Initialize the data class.""" self.systems = await self._api.get_systems() @@ -323,7 +386,8 @@ class SimpliSafe: async def update_system(system): """Update a system.""" await system.update() - self.last_event_data[system.system_id] = await system.get_latest_event() + _LOGGER.debug(f"Updated REST API data for system {system.system_id}") + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id)) tasks = [update_system(system) for system in self.systems.values()] @@ -371,8 +435,52 @@ class SimpliSafe: if self._emergency_refresh_token_used: self._emergency_refresh_token_used = False - _LOGGER.debug("Updated data for all SimpliSafe systems") - async_dispatcher_send(self._hass, TOPIC_UPDATE) + async def async_websocket_connect(self): + """Register handlers and connect to the websocket.""" + + async def _websocket_reconnect(event_time): + """Forcibly disconnect from and reconnect to the websocket.""" + _LOGGER.debug("Websocket watchdog expired; forcing socket reconnection") + await self._api.websocket.async_disconnect() + await self._attempt_websocket_connect() + + def on_connect(): + """Define a handler to fire when the websocket is connected.""" + _LOGGER.info("Connected to websocket") + _LOGGER.debug("Websocket watchdog starting") + if self._websocket_watchdog_listener is not None: + self._websocket_watchdog_listener() + self._websocket_watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _websocket_reconnect + ) + + def on_disconnect(): + """Define a handler to fire when the websocket is disconnected.""" + _LOGGER.info("Disconnected from websocket") + + def on_event(data): + """Define a handler to fire when a new SimpliSafe event arrives.""" + event = async_create_event_from_raw_data(data) + self.last_websocket_data[data["sid"]] = event + _LOGGER.debug(f'Updated websocket data for system {data["sid"]}') + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(data["sid"])) + + _LOGGER.debug("Resetting websocket watchdog") + self._websocket_watchdog_listener() + self._websocket_watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _websocket_reconnect + ) + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + + self._api.websocket.on_connect(on_connect) + self._api.websocket.on_disconnect(on_disconnect) + self._api.websocket.on_event(on_event) + + await self._attempt_websocket_connect() + + async def async_websocket_disconnect(self): + """Disconnect from the websocket.""" + await self._api.websocket.async_disconnect() class SimpliSafeEntity(Entity): @@ -382,8 +490,11 @@ class SimpliSafeEntity(Entity): """Initialize.""" self._async_unsub_dispatcher_connect = None self._attrs = {ATTR_SYSTEM_ID: system.system_id} + self._last_used_rest_api_data = None + self._last_used_websocket_data = None self._name = name self._online = True + self._state = None self._system = system if serial: @@ -436,9 +547,36 @@ class SimpliSafeEntity(Entity): self.async_schedule_update_ha_state(True) self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update + self.hass, TOPIC_UPDATE.format(self._system.system_id), update ) + async def async_update(self): + """Update the entity.""" + rest_data = self._simplisafe.last_rest_api_data.get(self._system.system_id) + ws_data = self._simplisafe.last_websocket_data.get(self._system.system_id) + + # If the most recent REST API data (within the data object) doesn't match what + # this entity last used, update: + if self._last_used_rest_api_data != rest_data: + self._last_used_rest_api_data = rest_data + self.async_update_from_rest_api(rest_data) + + # If the most recent websocket data (within the data object) doesn't match what + # this entity last used, update: + if self._last_used_websocket_data != ws_data: + self._last_used_websocket_data = ws_data + self.async_update_from_websocket_api(ws_data) + + @callback + def async_update_from_rest_api(data): + """Update the entity with the provided REST API data.""" + raise NotImplementedError() + + @callback + def async_update_from_websocket_api(data): + """Update the entity with the provided websocket API data.""" + pass + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 362c0244749..3101d107891 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -2,9 +2,21 @@ import logging import re -from simplipy.entity import EntityTypes +from simplipy.errors import SimplipyError from simplipy.system import SystemStates -from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF +from simplipy.websocket import ( + EVENT_ALARM_CANCELED, + EVENT_ALARM_TRIGGERED, + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + EVENT_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, +) from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -23,40 +35,34 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.core import callback from . import SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN +from .const import ( + ATTR_ALARM_DURATION, + ATTR_ALARM_VOLUME, + ATTR_CHIME_VOLUME, + ATTR_ENTRY_DELAY_AWAY, + ATTR_ENTRY_DELAY_HOME, + ATTR_EXIT_DELAY_AWAY, + ATTR_EXIT_DELAY_HOME, + ATTR_LAST_EVENT_TYPE, + ATTR_LIGHT, + ATTR_VOICE_PROMPT_VOLUME, + DATA_CLIENT, + DOMAIN, + VOLUME_STRING_MAP, +) _LOGGER = logging.getLogger(__name__) -ATTR_ALARM_DURATION = "alarm_duration" -ATTR_ALARM_VOLUME = "alarm_volume" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" -ATTR_CHIME_VOLUME = "chime_volume" -ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" -ATTR_ENTRY_DELAY_HOME = "entry_delay_home" -ATTR_EXIT_DELAY_AWAY = "exit_delay_away" -ATTR_EXIT_DELAY_HOME = "exit_delay_home" ATTR_GSM_STRENGTH = "gsm_strength" -ATTR_LAST_EVENT_INFO = "last_event_info" -ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" -ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" -ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" -ATTR_LAST_EVENT_TYPE = "last_event_type" -ATTR_LIGHT = "light" +ATTR_PIN_NAME = "pin_name" ATTR_RF_JAMMING = "rf_jamming" -ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" -VOLUME_STRING_MAP = { - VOLUME_HIGH: "high", - VOLUME_LOW: "low", - VOLUME_MEDIUM: "medium", - VOLUME_OFF: "off", -} - async def async_setup_entry(hass, entry, async_add_entities): """Set up a SimpliSafe alarm control panel based on a config entry.""" @@ -78,30 +84,25 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): super().__init__(system, "Alarm Control Panel") self._changed_by = None self._code = code + self._last_event = None self._simplisafe = simplisafe - self._state = None - if self._system.version == 3: - self._attrs.update( - { - ATTR_ALARM_DURATION: self._system.alarm_duration, - ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], - ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, - ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], - ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, - ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, - ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, - ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, - ATTR_GSM_STRENGTH: self._system.gsm_strength, - ATTR_LIGHT: self._system.light, - ATTR_RF_JAMMING: self._system.rf_jamming, - ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ - self._system.voice_prompt_volume - ], - ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, - ATTR_WIFI_STRENGTH: self._system.wifi_strength, - } - ) + if system.alarm_going_off: + self._state = STATE_ALARM_TRIGGERED + elif system.state == SystemStates.away: + self._state = STATE_ALARM_ARMED_AWAY + elif system.state in ( + SystemStates.away_count, + SystemStates.exit_delay, + SystemStates.home_count, + ): + self._state = STATE_ALARM_ARMING + elif system.state == SystemStates.home: + self._state = STATE_ALARM_ARMED_HOME + elif system.state == SystemStates.off: + self._state = STATE_ALARM_DISARMED + else: + self._state = None @property def changed_by(self): @@ -139,71 +140,100 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): if not self._validate_code(code, "disarming"): return - await self._system.set_off() + try: + await self._system.set_off() + except SimplipyError as err: + _LOGGER.error('Error while disarming "%s": %s', self._system.name, err) + return + + self._state = STATE_ALARM_DISARMED async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if not self._validate_code(code, "arming home"): return - await self._system.set_home() + try: + await self._system.set_home() + except SimplipyError as err: + _LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err) + return + + self._state = STATE_ALARM_ARMED_HOME async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if not self._validate_code(code, "arming away"): return - await self._system.set_away() + try: + await self._system.set_away() + except SimplipyError as err: + _LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err) + return - async def async_update(self): - """Update alarm status.""" - last_event = self._simplisafe.last_event_data[self._system.system_id] - - if last_event.get("pinName"): - self._changed_by = last_event["pinName"] + self._state = STATE_ALARM_ARMING + @callback + def async_update_from_rest_api(self, data): + """Update the entity with the provided REST API data.""" if self._system.state == SystemStates.error: self._online = False return self._online = True - if self._system.alarm_going_off: + if self._system.version == 3: + self._attrs.update( + { + ATTR_ALARM_DURATION: self._system.alarm_duration, + ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], + ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, + ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], + ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, + ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, + ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, + ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, + ATTR_GSM_STRENGTH: self._system.gsm_strength, + ATTR_LIGHT: self._system.light, + ATTR_RF_JAMMING: self._system.rf_jamming, + ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ + self._system.voice_prompt_volume + ], + ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, + ATTR_WIFI_STRENGTH: self._system.wifi_strength, + } + ) + + @callback + def async_update_from_websocket_api(self, data): + """Update the entity with the provided websocket API data.""" + if data.get(ATTR_PIN_NAME): + self._changed_by = data[ATTR_PIN_NAME] + + if data[ATTR_LAST_EVENT_TYPE] in ( + EVENT_ALARM_CANCELED, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + ): + self._state = STATE_ALARM_DISARMED + elif data[ATTR_LAST_EVENT_TYPE] == EVENT_ALARM_TRIGGERED: self._state = STATE_ALARM_TRIGGERED - elif self._system.state == SystemStates.away: + elif data[ATTR_LAST_EVENT_TYPE] in ( + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + ): self._state = STATE_ALARM_ARMED_AWAY - elif self._system.state in ( - SystemStates.away_count, - SystemStates.exit_delay, - SystemStates.home_count, + elif data[ATTR_LAST_EVENT_TYPE] == EVENT_ARMED_HOME: + self._state = STATE_ALARM_ARMED_HOME + elif data[ATTR_LAST_EVENT_TYPE] in ( + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, ): self._state = STATE_ALARM_ARMING - elif self._system.state == SystemStates.home: - self._state = STATE_ALARM_ARMED_HOME - elif self._system.state == SystemStates.off: - self._state = STATE_ALARM_DISARMED else: self._state = None - try: - last_event_sensor_type = EntityTypes(last_event["sensorType"]).name - except ValueError: - _LOGGER.warning( - 'Encountered unknown entity type: %s ("%s"). Please report it at' - "https://github.com/home-assistant/home-assistant/issues.", - last_event["sensorType"], - last_event["sensorName"], - ) - last_event_sensor_type = None - - self._attrs.update( - { - ATTR_LAST_EVENT_INFO: last_event["info"], - ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: last_event_sensor_type, - ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( - last_event["eventTimestamp"] - ), - ATTR_LAST_EVENT_TYPE: last_event["eventType"], - } - ) + self._attrs.update(data) diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py index 4dfef39de46..bf808f201e8 100644 --- a/homeassistant/components/simplisafe/const.py +++ b/homeassistant/components/simplisafe/const.py @@ -1,10 +1,33 @@ """Define constants for the SimpliSafe component.""" from datetime import timedelta +from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF + DOMAIN = "simplisafe" DATA_CLIENT = "client" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) -TOPIC_UPDATE = "update" +ATTR_ALARM_DURATION = "alarm_duration" +ATTR_ALARM_VOLUME = "alarm_volume" +ATTR_CHIME_VOLUME = "chime_volume" +ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" +ATTR_ENTRY_DELAY_HOME = "entry_delay_home" +ATTR_EXIT_DELAY_AWAY = "exit_delay_away" +ATTR_EXIT_DELAY_HOME = "exit_delay_home" +ATTR_LAST_EVENT_INFO = "last_event_info" +ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" +ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" +ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" +ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_LIGHT = "light" +ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" + +VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] +VOLUME_STRING_MAP = { + VOLUME_HIGH: "high", + VOLUME_LOW: "low", + VOLUME_MEDIUM: "medium", + VOLUME_OFF: "off", +} diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 10c5d310e73..6da8763d4c1 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,13 +1,16 @@ """Support for SimpliSafe locks.""" import logging +from simplipy.errors import SimplipyError from simplipy.lock import LockStates +from simplipy.websocket import EVENT_LOCK_ERROR, EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED from homeassistant.components.lock import LockDevice from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED +from homeassistant.core import callback from . import SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN +from .const import ATTR_LAST_EVENT_TYPE, DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -15,12 +18,6 @@ ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_JAMMED = "jammed" ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" -STATE_MAP = { - LockStates.locked: STATE_LOCKED, - LockStates.unknown: STATE_UNKNOWN, - LockStates.unlocked: STATE_UNLOCKED, -} - async def async_setup_entry(hass, entry, async_add_entities): """Set up SimpliSafe locks based on a config entry.""" @@ -45,24 +42,36 @@ class SimpliSafeLock(SimpliSafeEntity, LockDevice): @property def is_locked(self): """Return true if the lock is locked.""" - return STATE_MAP.get(self._lock.state) == STATE_LOCKED + return self._state == STATE_LOCKED async def async_lock(self, **kwargs): """Lock the lock.""" - await self._lock.lock() + try: + await self._lock.lock() + except SimplipyError as err: + _LOGGER.error('Error while locking "%s": %s', self._lock.name, err) + return + + self._state = STATE_LOCKED async def async_unlock(self, **kwargs): """Unlock the lock.""" - await self._lock.unlock() + try: + await self._lock.unlock() + except SimplipyError as err: + _LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) + return - async def async_update(self): - """Update lock status.""" + self._state = STATE_UNLOCKED + + @callback + def async_update_from_rest_api(self, data): + """Update the entity with the provided REST API data.""" if self._lock.offline or self._lock.disabled: self._online = False return self._online = True - self._attrs.update( { ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery, @@ -70,3 +79,17 @@ class SimpliSafeLock(SimpliSafeEntity, LockDevice): ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery, } ) + + @callback + def async_update_from_websocket_api(self, data): + """Update the entity with the provided websocket API data.""" + if data[ATTR_LAST_EVENT_TYPE] == EVENT_LOCK_LOCKED: + self._state = STATE_LOCKED + elif data[ATTR_LAST_EVENT_TYPE] == EVENT_LOCK_UNLOCKED: + self._state = STATE_UNLOCKED + elif data[ATTR_LAST_EVENT_TYPE] == EVENT_LOCK_ERROR: + self._state = STATE_UNKNOWN + else: + self._state = None + + self._attrs.update(data) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index f95db72d45a..3b04d26732c 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==6.1.0"], + "requirements": ["simplisafe-python==7.1.0"], "dependencies": [], "codeowners": ["@bachya"] }