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 import API
from simplipy.errors import InvalidCredentialsError, SimplipyError from simplipy.errors import InvalidCredentialsError, SimplipyError
from simplipy.system.v3 import LevelMap as V3Volume
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.config_entries import SOURCE_IMPORT
@ -14,6 +15,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_TOKEN, CONF_TOKEN,
CONF_USERNAME, CONF_USERNAME,
STATE_HOME,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
@ -35,27 +37,57 @@ from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE
_LOGGER = logging.getLogger(__name__) _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" CONF_ACCOUNTS = "accounts"
DATA_LISTENER = "listener" 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_ARRIVAL_STATE): vol.In((STATE_AWAY, STATE_HOME)),
vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, 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_VOLUME_PROPERTY): vol.In(
vol.Required(ATTR_PIN_LABEL): cv.string, (VOLUME_PROPERTY_ALARM, VOLUME_PROPERTY_CHIME, VOLUME_PROPERTY_VOICE_PROMPT)
vol.Required(ATTR_PIN_VALUE): cv.string, ),
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) _async_save_refresh_token(hass, config_entry, api.refresh_token)
systems = await api.get_systems() systems = await api.get_systems()
simplisafe = SimpliSafe(hass, config_entry, systems) simplisafe = SimpliSafe(hass, api, systems, config_entry)
await simplisafe.async_update() await simplisafe.async_update()
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe 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) 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 @_verify_domain_control
async def remove_pin(call): async def remove_pin(call):
"""Remove a PIN.""" """Remove a PIN."""
system = systems[int(call.data[ATTR_SYSTEM_ID])] system = systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) 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 @_verify_domain_control
async def set_pin(call): async def set_pin(call):
"""Set a PIN.""" """Set a PIN."""
system = systems[int(call.data[ATTR_SYSTEM_ID])] system = systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) 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 [ for service, method, schema in [
("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), ("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_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) hass.services.async_register(DOMAIN, service, method, schema=schema)
@ -215,8 +348,9 @@ async def async_unload_entry(hass, entry):
class SimpliSafe: class SimpliSafe:
"""Define a SimpliSafe API object.""" """Define a SimpliSafe API object."""
def __init__(self, hass, config_entry, systems): def __init__(self, hass, api, systems, config_entry):
"""Initialize.""" """Initialize."""
self._api = api
self._config_entry = config_entry self._config_entry = config_entry
self._hass = hass self._hass = hass
self.last_event_data = {} self.last_event_data = {}
@ -238,9 +372,9 @@ class SimpliSafe:
self.last_event_data[system.system_id] = latest_event 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( _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): async def async_update(self):

View File

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

View File

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

View File

@ -10,11 +10,46 @@ remove_pin:
label_or_pin: label_or_pin:
description: The label/value to remove. description: The label/value to remove.
example: Test PIN 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: set_pin:
description: Set/update a PIN description: Set/update a PIN
fields: fields:
system_id: system_id:
description: The SimpliSafe system ID to affect. description: The SimpliSafe system ID to affect
example: 123987 example: 123987
label: label:
description: The label of the PIN description: The label of the PIN
@ -22,3 +57,15 @@ set_pin:
pin: pin:
description: The value of the PIN description: The value of the PIN
example: 1256 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 simplepush==1.1.4
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==5.2.0 simplisafe-python==5.3.5
# homeassistant.components.sisyphus # homeassistant.components.sisyphus
sisyphus-control==2.2.1 sisyphus-control==2.2.1

View File

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