diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index d57e7b83fa4..94da65ee51a 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -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): diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 96f3fa05f6b..9671d56c873 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -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, diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 61a16f8aa44..4115ce455b5 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -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": [ diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 52e66a435c6..d8a4973b49e 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 808b26f86a4..477cf1314de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfeba689189..06be8aac043 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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