Add support for real-time data from SimpliSafe (#31424)

* Add support for real-time data from SimpliSafe

* Updated requirements

* Linting

* Ensure dispatcher topic contains the domain

* Don't bother with a partial

* Websovket dataclass and other code review

* Ensure initial_event_to_use works with error

* Don't inline methods

* Don't abuse loop variable

* Simplify initial event retrieval

* Add connection lost and restored events

* Revert "Add connection lost and restored events"

This reverts commit e7ffe05938.

* Make _on_disconnect a static method

* Code review comments

* Allow entities to opt out of REST and/or websocket API updates

* Revert "Allow entities to opt out of REST and/or websocket API updates"

This reverts commit 1989f2e00e.

* Code review comments

* Fix issues with events not triggering correct entities

* Bug fixes
This commit is contained in:
Aaron Bach 2020-02-13 12:30:38 -07:00 committed by GitHub
parent 9e7185c676
commit f091e0412f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 451 additions and 137 deletions

View file

@ -1,10 +1,18 @@
"""Support for SimpliSafe alarm systems."""
import asyncio
from dataclasses import dataclass, field
from datetime import datetime
import logging
from typing import Optional
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 (
EVENT_LOCK_LOCKED,
EVENT_LOCK_UNLOCKED,
get_event_type_from_payload,
)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
@ -21,36 +29,50 @@ 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_LIGHT,
ATTR_VOICE_PROMPT_VOLUME,
DATA_CLIENT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
VOLUMES,
)
_LOGGER = logging.getLogger(__name__)
CONF_ACCOUNTS = "accounts"
DATA_LISTENER = "listener"
TOPIC_UPDATE = "simplisafe_update_data_{0}"
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"
DEFAULT_SOCKET_MIN_RETRY = 15
DEFAULT_WATCHDOG_SECONDS = 5 * 60
WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED]
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_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})
@ -283,8 +305,133 @@ async def async_unload_entry(hass, entry):
return True
@dataclass(frozen=True)
class SimpliSafeWebsocketEvent:
"""Define a representation of a parsed websocket event."""
event_data: dict
changed_by: Optional[str] = field(init=False)
event_type: Optional[str] = field(init=False)
info: str = field(init=False)
sensor_name: str = field(init=False)
sensor_serial: str = field(init=False)
sensor_type: EntityTypes = field(init=False)
system_id: int = field(init=False)
timestamp: datetime = field(init=False)
def __post_init__(self):
"""Initialize."""
object.__setattr__(self, "changed_by", self.event_data["pinName"])
object.__setattr__(
self, "event_type", get_event_type_from_payload(self.event_data)
)
object.__setattr__(self, "info", self.event_data["info"])
object.__setattr__(self, "sensor_name", self.event_data["sensorName"])
object.__setattr__(self, "sensor_serial", self.event_data["sensorSerial"])
try:
object.__setattr__(
self, "sensor_type", EntityTypes(self.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.",
self.event_data["sensorType"],
self.event_data["sensorName"],
)
object.__setattr__(self, "sensor_type", None)
object.__setattr__(self, "system_id", self.event_data["sid"])
object.__setattr__(
self, "timestamp", utc_from_timestamp(self.event_data["eventTimestamp"])
)
class SimpliSafeWebsocket:
"""Define a SimpliSafe websocket "manager" object."""
def __init__(self, hass, websocket):
"""Initialize."""
self._hass = hass
self._websocket = websocket
self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self._websocket_reconnect_underway = False
self._websocket_watchdog_listener = None
self.last_events = {}
async def _async_attempt_websocket_connect(self):
"""Attempt to connect to the websocket (retrying later on fail)."""
self._websocket_reconnect_underway = True
try:
await self._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.async_websocket_connect,
)
else:
self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self._websocket_reconnect_underway = False
async def _async_websocket_reconnect(self, event_time):
"""Forcibly disconnect from and reconnect to the websocket."""
_LOGGER.debug("Websocket watchdog expired; forcing socket reconnection")
await self.async_websocket_disconnect()
await self._async_attempt_websocket_connect()
def _on_connect(self):
"""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, self._async_websocket_reconnect
)
@staticmethod
def _on_disconnect():
"""Define a handler to fire when the websocket is disconnected."""
_LOGGER.info("Disconnected from websocket")
def _on_event(self, data):
"""Define a handler to fire when a new SimpliSafe event arrives."""
event = SimpliSafeWebsocketEvent(data)
_LOGGER.debug("New websocket event: %s", event)
self.last_events[data["sid"]] = event
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, self._async_websocket_reconnect
)
self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
async def async_websocket_connect(self):
"""Register handlers and connect to the websocket."""
if self._websocket_reconnect_underway:
return
self._websocket.on_connect(self._on_connect)
self._websocket.on_disconnect(self._on_disconnect)
self._websocket.on_event(self._on_event)
await self._async_attempt_websocket_connect()
async def async_websocket_disconnect(self):
"""Disconnect from the websocket."""
await self._websocket.async_disconnect()
class SimpliSafe:
"""Define a SimpliSafe API object."""
"""Define a SimpliSafe data object."""
def __init__(self, hass, api, config_entry):
"""Initialize."""
@ -292,14 +439,15 @@ class SimpliSafe:
self._config_entry = config_entry
self._emergency_refresh_token_used = False
self._hass = hass
self.last_event_data = {}
self.initial_event_to_use = {}
self.systems = None
self.websocket = SimpliSafeWebsocket(hass, api.websocket)
async def async_init(self):
"""Initialize the data class."""
self.systems = await self._api.get_systems()
asyncio.create_task(self.websocket.async_websocket_connect())
# Register the base station for each system:
self.systems = await self._api.get_systems()
for system in self.systems.values():
self._hass.async_create_task(
async_register_base_station(
@ -307,6 +455,17 @@ class SimpliSafe:
)
)
# Future events will come from the websocket, but since subscription to the
# websocket doesn't provide the most recent event, we grab it from the REST
# API to ensure event-related attributes aren't empty on startup:
try:
self.initial_event_to_use[
system.system_id
] = await system.get_latest_event()
except SimplipyError as err:
_LOGGER.error("Error while fetching initial event: %s", err)
self.initial_event_to_use[system.system_id] = {}
async def refresh(event_time):
"""Refresh data from the SimpliSafe account."""
await self.async_update()
@ -323,7 +482,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('Updated REST API data for "%s"', system.address)
async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id))
tasks = [update_system(system) for system in self.systems.values()]
@ -371,26 +531,41 @@ 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)
class SimpliSafeEntity(Entity):
"""Define a base SimpliSafe entity."""
def __init__(self, system, name, *, serial=None):
def __init__(self, simplisafe, system, name, *, serial=None):
"""Initialize."""
self._async_unsub_dispatcher_connect = None
self._attrs = {ATTR_SYSTEM_ID: system.system_id}
self._last_processed_websocket_event = None
self._name = name
self._online = True
self._simplisafe = simplisafe
self._system = system
self.websocket_events_to_listen_for = []
if serial:
self._serial = serial
else:
self._serial = system.serial
self._attrs = {
ATTR_LAST_EVENT_INFO: simplisafe.initial_event_to_use[system.system_id].get(
"info"
),
ATTR_LAST_EVENT_SENSOR_NAME: simplisafe.initial_event_to_use[
system.system_id
].get("sensorName"),
ATTR_LAST_EVENT_SENSOR_TYPE: simplisafe.initial_event_to_use[
system.system_id
].get("sensorType"),
ATTR_LAST_EVENT_TIMESTAMP: simplisafe.initial_event_to_use[
system.system_id
].get("eventTimestamp"),
ATTR_SYSTEM_ID: system.system_id,
}
@property
def available(self):
"""Return whether the entity is available."""
@ -427,6 +602,36 @@ class SimpliSafeEntity(Entity):
"""Return the unique ID of the entity."""
return self._serial
@callback
def _async_should_ignore_websocket_event(self, event):
"""Return whether this entity should ignore a particular websocket event.
Note that we can't check for a final condition whether the event belongs to
a particular entity, like a lock because some events (like arming the system
from a keypad _or_ from the website) should impact the same entity.
"""
# We've already processed this event:
if self._last_processed_websocket_event == event:
return True
# This is an event for a system other than the one this entity belongs to:
if event.system_id != self._system.system_id:
return True
# This isn't an event that this entity cares about:
if event.event_type not in self.websocket_events_to_listen_for:
return True
# This event is targeted at a specific entity whose serial number is different
# from this one's:
if (
event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL
and event.sensor_serial != self._serial
):
return True
return False
async def async_added_to_hass(self):
"""Register callbacks."""
@ -436,9 +641,41 @@ 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."""
self.async_update_from_rest_api()
last_websocket_event = self._simplisafe.websocket.last_events.get(
self._system.system_id
)
if self._async_should_ignore_websocket_event(last_websocket_event):
return
self._last_processed_websocket_event = last_websocket_event
self._attrs.update(
{
ATTR_LAST_EVENT_INFO: last_websocket_event.info,
ATTR_LAST_EVENT_SENSOR_NAME: last_websocket_event.sensor_name,
ATTR_LAST_EVENT_SENSOR_TYPE: last_websocket_event.sensor_type,
ATTR_LAST_EVENT_TIMESTAMP: last_websocket_event.timestamp,
}
)
self.async_update_from_websocket_event(last_websocket_event)
@callback
def async_update_from_rest_api(self):
"""Update the entity with the provided REST API data."""
pass
@callback
def async_update_from_websocket_event(self, event):
"""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:

View file

@ -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,33 @@ 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_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."""
@ -75,33 +80,42 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
def __init__(self, simplisafe, system, code):
"""Initialize the SimpliSafe alarm."""
super().__init__(system, "Alarm Control Panel")
super().__init__(simplisafe, system, "Alarm Control Panel")
self._changed_by = None
self._code = code
self._simplisafe = simplisafe
self._last_event = None
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
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,
}
)
for event_type in (
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,
):
self.websocket_events_to_listen_for.append(event_type)
@property
def changed_by(self):
@ -139,71 +153,96 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
if not self._validate_code(code, "disarming"):
return
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
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
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):
"""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_event(self, event):
"""Update the entity with the provided websocket API event data."""
if event.event_type in (
EVENT_ALARM_CANCELED,
EVENT_DISARMED_BY_MASTER_PIN,
EVENT_DISARMED_BY_REMOTE,
):
self._state = STATE_ALARM_DISARMED
elif event.event_type == EVENT_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
elif self._system.state == SystemStates.away:
elif event.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 event.event_type == EVENT_ARMED_HOME:
self._state = STATE_ALARM_ARMED_HOME
elif event.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._changed_by = event.changed_by

View file

@ -1,10 +1,28 @@
"""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_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",
}

View file

@ -1,10 +1,12 @@
"""Support for SimpliSafe locks."""
import logging
from simplipy.errors import SimplipyError
from simplipy.lock import LockStates
from simplipy.websocket import 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
@ -15,19 +17,13 @@ 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."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
async_add_entities(
[
SimpliSafeLock(system, lock)
SimpliSafeLock(simplisafe, system, lock)
for system in simplisafe.systems.values()
for lock in system.locks.values()
]
@ -37,32 +33,48 @@ async def async_setup_entry(hass, entry, async_add_entities):
class SimpliSafeLock(SimpliSafeEntity, LockDevice):
"""Define a SimpliSafe lock."""
def __init__(self, system, lock):
def __init__(self, simplisafe, system, lock):
"""Initialize."""
super().__init__(system, lock.name, serial=lock.serial)
super().__init__(simplisafe, system, lock.name, serial=lock.serial)
self._is_locked = False
self._lock = lock
for event_type in (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED):
self.websocket_events_to_listen_for.append(event_type)
@property
def is_locked(self):
"""Return true if the lock is locked."""
return STATE_MAP.get(self._lock.state) == STATE_LOCKED
return self._is_locked
async def async_lock(self, **kwargs):
"""Lock the lock."""
try:
await self._lock.lock()
except SimplipyError as err:
_LOGGER.error('Error while locking "%s": %s', self._lock.name, err)
return
self._is_locked = True
async def async_unlock(self, **kwargs):
"""Unlock the lock."""
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._is_locked = False
@callback
def async_update_from_rest_api(self):
"""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 +82,11 @@ class SimpliSafeLock(SimpliSafeEntity, LockDevice):
ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery,
}
)
@callback
def async_update_from_websocket_event(self, event):
"""Update the entity with the provided websocket event data."""
if event.event_type == EVENT_LOCK_LOCKED:
self._is_locked = True
else:
self._is_locked = False

View file

@ -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"]
}

View file

@ -1829,7 +1829,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==6.1.0
simplisafe-python==7.1.0
# homeassistant.components.sisyphus
sisyphus-control==2.2.1

View file

@ -622,7 +622,7 @@ sentry-sdk==0.13.5
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==6.1.0
simplisafe-python==7.1.0
# homeassistant.components.sleepiq
sleepyq==0.7