Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new properties and services for V3 SimpliSafe systems #28997

Merged
merged 9 commits into from Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
168 changes: 151 additions & 17 deletions homeassistant/components/simplisafe/__init__.py
Expand Up @@ -5,6 +5,7 @@

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
Expand All @@ -14,6 +15,7 @@
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_USERNAME,
STATE_HOME,
)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
Expand All @@ -35,27 +37,57 @@

_LOGGER = logging.getLogger(__name__)

CONF_ACCOUNTS = "accounts"

DATA_LISTENER = "listener"

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"

CONF_ACCOUNTS = "accounts"
STATE_AWAY = "away"
STATE_ENTRY = "entry"
STATE_EXIT = "exit"

DATA_LISTENER = "listener"
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 = vol.Schema(
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,
}
)

Expand Down Expand Up @@ -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

Expand All @@ -175,21 +207,122 @@ async def refresh(event_time):
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_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_light", set_light, SERVICE_SET_LIGHT_SCHEMA),
bachya marked this conversation as resolved.
Show resolved Hide resolved
("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)

Expand All @@ -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 = {}
Expand All @@ -238,9 +372,9 @@ async def _update_system(self, system):

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):
Expand Down
40 changes: 29 additions & 11 deletions homeassistant/components/simplisafe/alarm_control_panel.py
Expand Up @@ -24,14 +24,23 @@
_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"

Expand Down Expand Up @@ -64,16 +73,26 @@ def __init__(self, simplisafe, system, code):
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):
Expand Down Expand Up @@ -151,7 +170,6 @@ async def async_update(self):
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,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/simplisafe/manifest.json
Expand Up @@ -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": [
Expand Down
49 changes: 48 additions & 1 deletion homeassistant/components/simplisafe/services.yaml
Expand Up @@ -10,15 +10,62 @@ 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
example: Test 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
2 changes: 1 addition & 1 deletion requirements_all.txt
Expand Up @@ -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
Expand Down