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 commite7ffe05938
. * 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 commit1989f2e00e
. * Code review comments * Fix issues with events not triggering correct entities * Bug fixes
This commit is contained in:
parent
9e7185c676
commit
f091e0412f
7 changed files with 451 additions and 137 deletions
|
@ -1,10 +1,18 @@
|
||||||
"""Support for SimpliSafe alarm systems."""
|
"""Support for SimpliSafe alarm systems."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from simplipy import API
|
from simplipy import API
|
||||||
from simplipy.errors import InvalidCredentialsError, SimplipyError
|
from simplipy.entity import EntityTypes
|
||||||
from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF
|
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
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
|
@ -21,36 +29,50 @@ from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_send,
|
async_dispatcher_send,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import Entity
|
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 (
|
from homeassistant.helpers.service import (
|
||||||
async_register_admin_service,
|
async_register_admin_service,
|
||||||
verify_domain_control,
|
verify_domain_control,
|
||||||
)
|
)
|
||||||
|
from homeassistant.util.dt import utc_from_timestamp
|
||||||
|
|
||||||
from .config_flow import configured_instances
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ACCOUNTS = "accounts"
|
CONF_ACCOUNTS = "accounts"
|
||||||
|
|
||||||
DATA_LISTENER = "listener"
|
DATA_LISTENER = "listener"
|
||||||
|
TOPIC_UPDATE = "simplisafe_update_data_{0}"
|
||||||
|
|
||||||
ATTR_ALARM_DURATION = "alarm_duration"
|
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||||
ATTR_ALARM_VOLUME = "alarm_volume"
|
DEFAULT_WATCHDOG_SECONDS = 5 * 60
|
||||||
ATTR_CHIME_VOLUME = "chime_volume"
|
|
||||||
ATTR_ENTRY_DELAY_AWAY = "entry_delay_away"
|
WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED]
|
||||||
ATTR_ENTRY_DELAY_HOME = "entry_delay_home"
|
|
||||||
ATTR_EXIT_DELAY_AWAY = "exit_delay_away"
|
ATTR_LAST_EVENT_INFO = "last_event_info"
|
||||||
ATTR_EXIT_DELAY_HOME = "exit_delay_home"
|
ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name"
|
||||||
ATTR_LIGHT = "light"
|
ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type"
|
||||||
|
ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp"
|
||||||
ATTR_PIN_LABEL = "label"
|
ATTR_PIN_LABEL = "label"
|
||||||
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
|
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
|
||||||
ATTR_PIN_VALUE = "pin"
|
ATTR_PIN_VALUE = "pin"
|
||||||
ATTR_SYSTEM_ID = "system_id"
|
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})
|
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
|
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:
|
class SimpliSafe:
|
||||||
"""Define a SimpliSafe API object."""
|
"""Define a SimpliSafe data object."""
|
||||||
|
|
||||||
def __init__(self, hass, api, config_entry):
|
def __init__(self, hass, api, config_entry):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
|
@ -292,14 +439,15 @@ class SimpliSafe:
|
||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
self._emergency_refresh_token_used = False
|
self._emergency_refresh_token_used = False
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.last_event_data = {}
|
self.initial_event_to_use = {}
|
||||||
self.systems = None
|
self.systems = None
|
||||||
|
self.websocket = SimpliSafeWebsocket(hass, api.websocket)
|
||||||
|
|
||||||
async def async_init(self):
|
async def async_init(self):
|
||||||
"""Initialize the data class."""
|
"""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():
|
for system in self.systems.values():
|
||||||
self._hass.async_create_task(
|
self._hass.async_create_task(
|
||||||
async_register_base_station(
|
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):
|
async def refresh(event_time):
|
||||||
"""Refresh data from the SimpliSafe account."""
|
"""Refresh data from the SimpliSafe account."""
|
||||||
await self.async_update()
|
await self.async_update()
|
||||||
|
@ -323,7 +482,8 @@ class SimpliSafe:
|
||||||
async def update_system(system):
|
async def update_system(system):
|
||||||
"""Update a system."""
|
"""Update a system."""
|
||||||
await system.update()
|
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()]
|
tasks = [update_system(system) for system in self.systems.values()]
|
||||||
|
|
||||||
|
@ -371,26 +531,41 @@ class SimpliSafe:
|
||||||
if self._emergency_refresh_token_used:
|
if self._emergency_refresh_token_used:
|
||||||
self._emergency_refresh_token_used = False
|
self._emergency_refresh_token_used = False
|
||||||
|
|
||||||
_LOGGER.debug("Updated data for all SimpliSafe systems")
|
|
||||||
async_dispatcher_send(self._hass, TOPIC_UPDATE)
|
|
||||||
|
|
||||||
|
|
||||||
class SimpliSafeEntity(Entity):
|
class SimpliSafeEntity(Entity):
|
||||||
"""Define a base SimpliSafe entity."""
|
"""Define a base SimpliSafe entity."""
|
||||||
|
|
||||||
def __init__(self, system, name, *, serial=None):
|
def __init__(self, simplisafe, system, name, *, serial=None):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self._async_unsub_dispatcher_connect = None
|
self._async_unsub_dispatcher_connect = None
|
||||||
self._attrs = {ATTR_SYSTEM_ID: system.system_id}
|
self._last_processed_websocket_event = None
|
||||||
self._name = name
|
self._name = name
|
||||||
self._online = True
|
self._online = True
|
||||||
|
self._simplisafe = simplisafe
|
||||||
self._system = system
|
self._system = system
|
||||||
|
self.websocket_events_to_listen_for = []
|
||||||
|
|
||||||
if serial:
|
if serial:
|
||||||
self._serial = serial
|
self._serial = serial
|
||||||
else:
|
else:
|
||||||
self._serial = system.serial
|
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
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return whether the entity is available."""
|
"""Return whether the entity is available."""
|
||||||
|
@ -427,6 +602,36 @@ class SimpliSafeEntity(Entity):
|
||||||
"""Return the unique ID of the entity."""
|
"""Return the unique ID of the entity."""
|
||||||
return self._serial
|
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):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
|
|
||||||
|
@ -436,9 +641,41 @@ class SimpliSafeEntity(Entity):
|
||||||
self.async_schedule_update_ha_state(True)
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
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:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Disconnect dispatcher listener when removed."""
|
"""Disconnect dispatcher listener when removed."""
|
||||||
if self._async_unsub_dispatcher_connect:
|
if self._async_unsub_dispatcher_connect:
|
||||||
|
|
|
@ -2,9 +2,21 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from simplipy.entity import EntityTypes
|
from simplipy.errors import SimplipyError
|
||||||
from simplipy.system import SystemStates
|
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 (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
FORMAT_NUMBER,
|
FORMAT_NUMBER,
|
||||||
|
@ -23,40 +35,33 @@ from homeassistant.const import (
|
||||||
STATE_ALARM_DISARMED,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_TRIGGERED,
|
STATE_ALARM_TRIGGERED,
|
||||||
)
|
)
|
||||||
from homeassistant.util.dt import utc_from_timestamp
|
from homeassistant.core import callback
|
||||||
|
|
||||||
from . import SimpliSafeEntity
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_ALARM_DURATION = "alarm_duration"
|
|
||||||
ATTR_ALARM_VOLUME = "alarm_volume"
|
|
||||||
ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level"
|
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_GSM_STRENGTH = "gsm_strength"
|
||||||
ATTR_LAST_EVENT_INFO = "last_event_info"
|
ATTR_PIN_NAME = "pin_name"
|
||||||
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_RF_JAMMING = "rf_jamming"
|
ATTR_RF_JAMMING = "rf_jamming"
|
||||||
ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume"
|
|
||||||
ATTR_WALL_POWER_LEVEL = "wall_power_level"
|
ATTR_WALL_POWER_LEVEL = "wall_power_level"
|
||||||
ATTR_WIFI_STRENGTH = "wifi_strength"
|
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):
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
"""Set up a SimpliSafe alarm control panel based on a config entry."""
|
"""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):
|
def __init__(self, simplisafe, system, code):
|
||||||
"""Initialize the SimpliSafe alarm."""
|
"""Initialize the SimpliSafe alarm."""
|
||||||
super().__init__(system, "Alarm Control Panel")
|
super().__init__(simplisafe, system, "Alarm Control Panel")
|
||||||
self._changed_by = None
|
self._changed_by = None
|
||||||
self._code = code
|
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
|
self._state = None
|
||||||
|
|
||||||
if self._system.version == 3:
|
for event_type in (
|
||||||
self._attrs.update(
|
EVENT_ALARM_CANCELED,
|
||||||
{
|
EVENT_ALARM_TRIGGERED,
|
||||||
ATTR_ALARM_DURATION: self._system.alarm_duration,
|
EVENT_ARMED_AWAY,
|
||||||
ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume],
|
EVENT_ARMED_AWAY_BY_KEYPAD,
|
||||||
ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level,
|
EVENT_ARMED_AWAY_BY_REMOTE,
|
||||||
ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume],
|
EVENT_ARMED_HOME,
|
||||||
ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away,
|
EVENT_AWAY_EXIT_DELAY_BY_KEYPAD,
|
||||||
ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home,
|
EVENT_AWAY_EXIT_DELAY_BY_REMOTE,
|
||||||
ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away,
|
EVENT_DISARMED_BY_MASTER_PIN,
|
||||||
ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home,
|
EVENT_DISARMED_BY_REMOTE,
|
||||||
ATTR_GSM_STRENGTH: self._system.gsm_strength,
|
EVENT_HOME_EXIT_DELAY,
|
||||||
ATTR_LIGHT: self._system.light,
|
):
|
||||||
ATTR_RF_JAMMING: self._system.rf_jamming,
|
self.websocket_events_to_listen_for.append(event_type)
|
||||||
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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def changed_by(self):
|
def changed_by(self):
|
||||||
|
@ -139,71 +153,96 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
|
||||||
if not self._validate_code(code, "disarming"):
|
if not self._validate_code(code, "disarming"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
await self._system.set_off()
|
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):
|
async def async_alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if not self._validate_code(code, "arming home"):
|
if not self._validate_code(code, "arming home"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
await self._system.set_home()
|
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):
|
async def async_alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if not self._validate_code(code, "arming away"):
|
if not self._validate_code(code, "arming away"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
await self._system.set_away()
|
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):
|
self._state = STATE_ALARM_ARMING
|
||||||
"""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"]
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_from_rest_api(self):
|
||||||
|
"""Update the entity with the provided REST API data."""
|
||||||
if self._system.state == SystemStates.error:
|
if self._system.state == SystemStates.error:
|
||||||
self._online = False
|
self._online = False
|
||||||
return
|
return
|
||||||
|
|
||||||
self._online = True
|
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
|
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
|
self._state = STATE_ALARM_ARMED_AWAY
|
||||||
elif self._system.state in (
|
elif event.event_type == EVENT_ARMED_HOME:
|
||||||
SystemStates.away_count,
|
self._state = STATE_ALARM_ARMED_HOME
|
||||||
SystemStates.exit_delay,
|
elif event.event_type in (
|
||||||
SystemStates.home_count,
|
EVENT_AWAY_EXIT_DELAY_BY_KEYPAD,
|
||||||
|
EVENT_AWAY_EXIT_DELAY_BY_REMOTE,
|
||||||
|
EVENT_HOME_EXIT_DELAY,
|
||||||
):
|
):
|
||||||
self._state = STATE_ALARM_ARMING
|
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:
|
else:
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
try:
|
self._changed_by = event.changed_by
|
||||||
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"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,10 +1,28 @@
|
||||||
"""Define constants for the SimpliSafe component."""
|
"""Define constants for the SimpliSafe component."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF
|
||||||
|
|
||||||
DOMAIN = "simplisafe"
|
DOMAIN = "simplisafe"
|
||||||
|
|
||||||
DATA_CLIENT = "client"
|
DATA_CLIENT = "client"
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
|
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",
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
"""Support for SimpliSafe locks."""
|
"""Support for SimpliSafe locks."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from simplipy.errors import SimplipyError
|
||||||
from simplipy.lock import LockStates
|
from simplipy.lock import LockStates
|
||||||
|
from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED
|
||||||
|
|
||||||
from homeassistant.components.lock import LockDevice
|
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 . import SimpliSafeEntity
|
||||||
from .const import DATA_CLIENT, DOMAIN
|
from .const import DATA_CLIENT, DOMAIN
|
||||||
|
@ -15,19 +17,13 @@ ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
|
||||||
ATTR_JAMMED = "jammed"
|
ATTR_JAMMED = "jammed"
|
||||||
ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery"
|
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):
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
"""Set up SimpliSafe locks based on a config entry."""
|
"""Set up SimpliSafe locks based on a config entry."""
|
||||||
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
SimpliSafeLock(system, lock)
|
SimpliSafeLock(simplisafe, system, lock)
|
||||||
for system in simplisafe.systems.values()
|
for system in simplisafe.systems.values()
|
||||||
for lock in system.locks.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):
|
class SimpliSafeLock(SimpliSafeEntity, LockDevice):
|
||||||
"""Define a SimpliSafe lock."""
|
"""Define a SimpliSafe lock."""
|
||||||
|
|
||||||
def __init__(self, system, lock):
|
def __init__(self, simplisafe, system, lock):
|
||||||
"""Initialize."""
|
"""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
|
self._lock = lock
|
||||||
|
|
||||||
|
for event_type in (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED):
|
||||||
|
self.websocket_events_to_listen_for.append(event_type)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_locked(self):
|
def is_locked(self):
|
||||||
"""Return true if the lock is locked."""
|
"""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):
|
async def async_lock(self, **kwargs):
|
||||||
"""Lock the lock."""
|
"""Lock the lock."""
|
||||||
|
try:
|
||||||
await self._lock.lock()
|
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):
|
async def async_unlock(self, **kwargs):
|
||||||
"""Unlock the lock."""
|
"""Unlock the lock."""
|
||||||
|
try:
|
||||||
await self._lock.unlock()
|
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):
|
self._is_locked = False
|
||||||
"""Update lock status."""
|
|
||||||
|
@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:
|
if self._lock.offline or self._lock.disabled:
|
||||||
self._online = False
|
self._online = False
|
||||||
return
|
return
|
||||||
|
|
||||||
self._online = True
|
self._online = True
|
||||||
|
|
||||||
self._attrs.update(
|
self._attrs.update(
|
||||||
{
|
{
|
||||||
ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery,
|
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,
|
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
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "SimpliSafe",
|
"name": "SimpliSafe",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||||
"requirements": ["simplisafe-python==6.1.0"],
|
"requirements": ["simplisafe-python==7.1.0"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@bachya"]
|
"codeowners": ["@bachya"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1829,7 +1829,7 @@ simplehound==0.3
|
||||||
simplepush==1.1.4
|
simplepush==1.1.4
|
||||||
|
|
||||||
# homeassistant.components.simplisafe
|
# homeassistant.components.simplisafe
|
||||||
simplisafe-python==6.1.0
|
simplisafe-python==7.1.0
|
||||||
|
|
||||||
# homeassistant.components.sisyphus
|
# homeassistant.components.sisyphus
|
||||||
sisyphus-control==2.2.1
|
sisyphus-control==2.2.1
|
||||||
|
|
|
@ -622,7 +622,7 @@ sentry-sdk==0.13.5
|
||||||
simplehound==0.3
|
simplehound==0.3
|
||||||
|
|
||||||
# homeassistant.components.simplisafe
|
# homeassistant.components.simplisafe
|
||||||
simplisafe-python==6.1.0
|
simplisafe-python==7.1.0
|
||||||
|
|
||||||
# homeassistant.components.sleepiq
|
# homeassistant.components.sleepiq
|
||||||
sleepyq==0.7
|
sleepyq==0.7
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue