Add new properties and services for V3 SimpliSafe systems (#28997)

* Add new properties and services for V3 SimpliSafe systems

* Small semantic change

* Updated docstrings

* Semantics

* Streamlined adding V3 properties

* Re-add attribute

* Bump to 5.3.5

* Owner comments

* Correct coroutine name
This commit is contained in:
Aaron Bach 2019-11-26 11:44:40 -07:00 committed by GitHub
parent 595567ad82
commit 2cdd8ad15e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 234 additions and 35 deletions

View File

@ -5,6 +5,7 @@ from datetime import timedelta
from simplipy import API
from simplipy.errors import InvalidCredentialsError, SimplipyError
from simplipy.system.v3 import LevelMap as V3Volume
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
@ -14,6 +15,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_USERNAME,
STATE_HOME,
)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
@ -35,27 +37,57 @@ from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE
_LOGGER = logging.getLogger(__name__)
ATTR_PIN_LABEL = "label"
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
ATTR_PIN_VALUE = "pin"
ATTR_SYSTEM_ID = "system_id"
CONF_ACCOUNTS = "accounts"
DATA_LISTENER = "listener"
SERVICE_REMOVE_PIN_SCHEMA = vol.Schema(
ATTR_ARMED_LIGHT_STATE = "armed_light_state"
ATTR_ARRIVAL_STATE = "arrival_state"
ATTR_PIN_LABEL = "label"
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
ATTR_PIN_VALUE = "pin"
ATTR_SECONDS = "seconds"
ATTR_SYSTEM_ID = "system_id"
ATTR_TRANSITION = "transition"
ATTR_VOLUME = "volume"
ATTR_VOLUME_PROPERTY = "volume_property"
STATE_AWAY = "away"
STATE_ENTRY = "entry"
STATE_EXIT = "exit"
VOLUME_PROPERTY_ALARM = "alarm"
VOLUME_PROPERTY_CHIME = "chime"
VOLUME_PROPERTY_VOICE_PROMPT = "voice_prompt"
SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int})
SERVICE_REMOVE_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend(
{vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string}
)
SERVICE_SET_DELAY_SCHEMA = SERVICE_BASE_SCHEMA.extend(
{
vol.Required(ATTR_SYSTEM_ID): cv.string,
vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string,
vol.Required(ATTR_ARRIVAL_STATE): vol.In((STATE_AWAY, STATE_HOME)),
vol.Required(ATTR_TRANSITION): vol.In((STATE_ENTRY, STATE_EXIT)),
vol.Required(ATTR_SECONDS): cv.positive_int,
}
)
SERVICE_SET_PIN_SCHEMA = vol.Schema(
SERVICE_SET_LIGHT_SCHEMA = SERVICE_BASE_SCHEMA.extend(
{vol.Required(ATTR_ARMED_LIGHT_STATE): cv.boolean}
)
SERVICE_SET_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend(
{vol.Required(ATTR_PIN_LABEL): cv.string, vol.Required(ATTR_PIN_VALUE): cv.string}
)
SERVICE_SET_VOLUME_SCHEMA = SERVICE_BASE_SCHEMA.extend(
{
vol.Required(ATTR_SYSTEM_ID): cv.string,
vol.Required(ATTR_PIN_LABEL): cv.string,
vol.Required(ATTR_PIN_VALUE): cv.string,
vol.Required(ATTR_VOLUME_PROPERTY): vol.In(
(VOLUME_PROPERTY_ALARM, VOLUME_PROPERTY_CHIME, VOLUME_PROPERTY_VOICE_PROMPT)
),
vol.Required(ATTR_VOLUME): cv.string,
}
)
@ -150,7 +182,7 @@ async def async_setup_entry(hass, config_entry):
_async_save_refresh_token(hass, config_entry, api.refresh_token)
systems = await api.get_systems()
simplisafe = SimpliSafe(hass, config_entry, systems)
simplisafe = SimpliSafe(hass, api, systems, config_entry)
await simplisafe.async_update()
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe
@ -175,21 +207,122 @@ async def async_setup_entry(hass, config_entry):
async_register_base_station(hass, system, config_entry.entry_id)
)
@callback
def verify_system_exists(coro):
"""Log an error if a service call uses an invalid system ID."""
async def decorator(call):
"""Decorate."""
system_id = int(call.data[ATTR_SYSTEM_ID])
if system_id not in systems:
_LOGGER.error("Unknown system ID in service call: %s", system_id)
return
await coro(call)
return decorator
@callback
def v3_only(coro):
"""Log an error if the decorated coroutine is called with a v2 system."""
async def decorator(call):
"""Decorate."""
system = systems[int(call.data[ATTR_SYSTEM_ID])]
if system.version != 3:
_LOGGER.error("Service only available on V3 systems")
return
await coro(call)
return decorator
@verify_system_exists
@_verify_domain_control
async def remove_pin(call):
"""Remove a PIN."""
system = systems[int(call.data[ATTR_SYSTEM_ID])]
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE])
system = systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE])
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@v3_only
@_verify_domain_control
async def set_alarm_duration(call):
"""Set the duration of a running alarm."""
system = systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.set_alarm_duration(call.data[ATTR_SECONDS])
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@v3_only
@_verify_domain_control
async def set_delay(call):
"""Set the delay duration for entry/exit, away/home (any combo)."""
system = systems[call.data[ATTR_SYSTEM_ID]]
coro = getattr(
system,
f"set_{call.data[ATTR_TRANSITION]}_delay_{call.data[ATTR_ARRIVAL_STATE]}",
)
try:
await coro(call.data[ATTR_SECONDS])
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@v3_only
@_verify_domain_control
async def set_armed_light(call):
"""Turn the base station light on/off."""
system = systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.set_light(call.data[ATTR_ARMED_LIGHT_STATE])
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@_verify_domain_control
async def set_pin(call):
"""Set a PIN."""
system = systems[int(call.data[ATTR_SYSTEM_ID])]
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE])
system = systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE])
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@v3_only
@_verify_domain_control
async def set_volume_property(call):
"""Set a volume parameter in an appropriate service call."""
system = systems[call.data[ATTR_SYSTEM_ID]]
try:
volume = V3Volume[call.data[ATTR_VOLUME]]
except KeyError:
_LOGGER.error("Unknown volume string: %s", call.data[ATTR_VOLUME])
return
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
return
else:
coro = getattr(system, f"set_{call.data[ATTR_VOLUME_PROPERTY]}_volume")
await coro(volume)
for service, method, schema in [
("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA),
("set_alarm_duration", set_alarm_duration, SERVICE_SET_DELAY_SCHEMA),
("set_delay", set_delay, SERVICE_SET_DELAY_SCHEMA),
("set_armed_light", set_armed_light, SERVICE_SET_LIGHT_SCHEMA),
("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA),
("set_volume_property", set_volume_property, SERVICE_SET_VOLUME_SCHEMA),
]:
hass.services.async_register(DOMAIN, service, method, schema=schema)
@ -215,8 +348,9 @@ async def async_unload_entry(hass, entry):
class SimpliSafe:
"""Define a SimpliSafe API object."""
def __init__(self, hass, config_entry, systems):
def __init__(self, hass, api, systems, config_entry):
"""Initialize."""
self._api = api
self._config_entry = config_entry
self._hass = hass
self.last_event_data = {}
@ -238,9 +372,9 @@ class SimpliSafe:
self.last_event_data[system.system_id] = latest_event
if system.api.refresh_token_dirty:
if self._api.refresh_token_dirty:
_async_save_refresh_token(
self._hass, self._config_entry, system.api.refresh_token
self._hass, self._config_entry, self._api.refresh_token
)
async def async_update(self):

View File

@ -28,14 +28,23 @@ from .const import DATA_CLIENT, DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTR_ALARM_ACTIVE = "alarm_active"
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_RF_JAMMING = "rf_jamming"
ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume"
ATTR_WALL_POWER_LEVEL = "wall_power_level"
ATTR_WIFI_STRENGTH = "wifi_strength"
@ -68,16 +77,26 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
self._simplisafe = simplisafe
self._state = None
# Some properties only exist for V2 or V3 systems:
for prop in (
ATTR_BATTERY_BACKUP_POWER_LEVEL,
ATTR_GSM_STRENGTH,
ATTR_RF_JAMMING,
ATTR_WALL_POWER_LEVEL,
ATTR_WIFI_STRENGTH,
):
if hasattr(system, prop):
self._attrs[prop] = getattr(system, prop)
self._attrs.update({ATTR_ALARM_ACTIVE: self._system.alarm_going_off})
if self._system.version == 3:
self._attrs.update(
{
ATTR_ALARM_DURATION: self._system.alarm_duration,
ATTR_ALARM_VOLUME: self._system.alarm_volume.name,
ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level,
ATTR_CHIME_VOLUME: self._system.chime_volume.name,
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: self._system.voice_prompt_volume.name,
ATTR_WALL_POWER_LEVEL: self._system.wall_power_level,
ATTR_WIFI_STRENGTH: self._system.wifi_strength,
}
)
@property
def changed_by(self):
@ -160,7 +179,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
last_event = self._simplisafe.last_event_data[self._system.system_id]
self._attrs.update(
{
ATTR_ALARM_ACTIVE: self._system.alarm_going_off,
ATTR_LAST_EVENT_INFO: last_event["info"],
ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"],
ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name,

View File

@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": [
"simplisafe-python==5.2.0"
"simplisafe-python==5.3.5"
],
"dependencies": [],
"codeowners": [

View File

@ -10,11 +10,46 @@ remove_pin:
label_or_pin:
description: The label/value to remove.
example: Test PIN
set_alarm_duration:
description: "Set the duration (in seconds) of an active alarm"
fields:
system_id:
description: The SimpliSafe system ID to affect
example: 123987
seconds:
description: The number of seconds to sound the alarm
example: 120
set_delay:
description: >
Set a duration for how long the base station should delay when transitioning
between states
fields:
system_id:
description: The SimpliSafe system ID to affect
example: 123987
arrival_state:
description: The target "arrival" state (away, home)
example: away
transition:
description: The system state transition to affect (entry, exit)
example: exit
seconds:
description: "The number of seconds to delay"
example: 120
set_light:
description: "Turn the base station light on/off"
fields:
system_id:
description: The SimpliSafe system ID to affect
example: 123987
armed_light_state:
description: "True for on, False for off"
example: "True"
set_pin:
description: Set/update a PIN
fields:
system_id:
description: The SimpliSafe system ID to affect.
description: The SimpliSafe system ID to affect
example: 123987
label:
description: The label of the PIN
@ -22,3 +57,15 @@ set_pin:
pin:
description: The value of the PIN
example: 1256
set_volume_property:
description: Set a level for one of the base station's various volumes
fields:
system_id:
description: The SimpliSafe system ID to affect
example: 123987
volume_property:
description: The volume property to set (alarm, chime, voice_prompt)
example: voice_prompt
volume:
description: "A volume (off, low, medium, high)"
example: low

View File

@ -1788,7 +1788,7 @@ shodan==1.20.0
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==5.2.0
simplisafe-python==5.3.5
# homeassistant.components.sisyphus
sisyphus-control==2.2.1

View File

@ -556,7 +556,7 @@ rxv==0.6.0
samsungctl[websocket]==0.7.1
# homeassistant.components.simplisafe
simplisafe-python==5.2.0
simplisafe-python==5.3.5
# homeassistant.components.sleepiq
sleepyq==0.7