From 2f17529f2833b69994870650430521b270bd69eb Mon Sep 17 00:00:00 2001 From: c-soft Date: Sat, 13 Apr 2019 14:24:12 +0200 Subject: [PATCH] Add Satel_integra switchable outputs and multiple partitions (#21992) * Added editable outputs and multiple zones. * Updated requirements_all.txt * Linter fixes. * Post-review changes * Fixed too many lines separation error * Passing satel controller as parameter to entities. * Fixed linter error. * Fixed forgotten requirements update. * Fixed satel_integra version (again!?!) * Fixed manifest.json. * Fixed passing non-serializable controller * Removed unnecessary isinstance check. * Post review changes --- .../components/satel_integra/__init__.py | 63 +++++++++--- .../satel_integra/alarm_control_panel.py | 47 +++++---- .../components/satel_integra/binary_sensor.py | 19 ++-- .../components/satel_integra/manifest.json | 2 +- .../components/satel_integra/switch.py | 97 +++++++++++++++++++ requirements_all.txt | 2 +- 6 files changed, 186 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/satel_integra/switch.py diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index 2aae9ea8dd9..ea1029e4fe0 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -1,9 +1,10 @@ """Support for Satel Integra devices.""" +import collections import logging import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -21,13 +22,14 @@ DOMAIN = 'satel_integra' DATA_SATEL = 'satel_integra' -CONF_DEVICE_PORT = 'port' -CONF_DEVICE_PARTITION = 'partition' +CONF_DEVICE_CODE = 'code' +CONF_DEVICE_PARTITIONS = 'partitions' CONF_ARM_HOME_MODE = 'arm_home_mode' CONF_ZONE_NAME = 'name' CONF_ZONE_TYPE = 'type' CONF_ZONES = 'zones' CONF_OUTPUTS = 'outputs' +CONF_SWITCHABLE_OUTPUTS = 'switchable_outputs' ZONES = 'zones' @@ -42,20 +44,38 @@ SIGNAL_OUTPUTS_UPDATED = 'satel_integra.outputs_updated' ZONE_SCHEMA = vol.Schema({ vol.Required(CONF_ZONE_NAME): cv.string, vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string}) +EDITABLE_OUTPUT_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_NAME): cv.string}) +PARTITION_SCHEMA = vol.Schema( + {vol.Required(CONF_ZONE_NAME): cv.string, + vol.Optional(CONF_ARM_HOME_MODE, default=DEFAULT_CONF_ARM_HOME_MODE): + vol.In([1, 2, 3]), + } + ) + + +def is_alarm_code_necessary(value): + """Check if alarm code must be configured.""" + if value.get(CONF_SWITCHABLE_OUTPUTS) and CONF_DEVICE_CODE not in value: + raise vol.Invalid('You need to specify alarm ' + ' code to use switchable_outputs') + + return value + CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + DOMAIN: vol.All({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEVICE_PARTITION, - default=DEFAULT_DEVICE_PARTITION): cv.positive_int, - vol.Optional(CONF_ARM_HOME_MODE, - default=DEFAULT_CONF_ARM_HOME_MODE): vol.In([1, 2, 3]), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DEVICE_CODE): cv.string, + vol.Optional(CONF_DEVICE_PARTITIONS, + default={}): {vol.Coerce(int): PARTITION_SCHEMA}, vol.Optional(CONF_ZONES, default={}): {vol.Coerce(int): ZONE_SCHEMA}, vol.Optional(CONF_OUTPUTS, default={}): {vol.Coerce(int): ZONE_SCHEMA}, - }), + vol.Optional(CONF_SWITCHABLE_OUTPUTS, + default={}): {vol.Coerce(int): EDITABLE_OUTPUT_SCHEMA}, + }, is_alarm_code_necessary), }, extra=vol.ALLOW_EXTRA) @@ -65,13 +85,20 @@ async def async_setup(hass, config): zones = conf.get(CONF_ZONES) outputs = conf.get(CONF_OUTPUTS) + switchable_outputs = conf.get(CONF_SWITCHABLE_OUTPUTS) host = conf.get(CONF_HOST) - port = conf.get(CONF_DEVICE_PORT) - partition = conf.get(CONF_DEVICE_PARTITION) + port = conf.get(CONF_PORT) + partitions = conf.get(CONF_DEVICE_PARTITIONS) from satel_integra.satel_integra import AsyncSatel - controller = AsyncSatel(host, port, hass.loop, zones, outputs, partition) + monitored_outputs = collections.OrderedDict( + list(outputs.items()) + + list(switchable_outputs.items()) + ) + + controller = AsyncSatel(host, port, hass.loop, + zones, monitored_outputs, partitions) hass.data[DATA_SATEL] = controller @@ -94,7 +121,15 @@ async def async_setup(hass, config): hass.async_create_task( async_load_platform(hass, 'binary_sensor', DOMAIN, - {CONF_ZONES: zones, CONF_OUTPUTS: outputs}, config) + {CONF_ZONES: zones, + CONF_OUTPUTS: outputs}, config) + ) + + hass.async_create_task( + async_load_platform(hass, 'switch', DOMAIN, + {CONF_SWITCHABLE_OUTPUTS: switchable_outputs, + CONF_DEVICE_CODE: conf.get(CONF_DEVICE_CODE)}, + config) ) @callback diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 02e683bac5a..a896d7e8061 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -11,7 +11,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - CONF_ARM_HOME_MODE, CONF_DEVICE_PARTITION, DATA_SATEL, + CONF_ARM_HOME_MODE, CONF_DEVICE_PARTITIONS, DATA_SATEL, CONF_ZONE_NAME, SIGNAL_PANEL_MESSAGE) _LOGGER = logging.getLogger(__name__) @@ -23,23 +23,34 @@ async def async_setup_platform( if not discovery_info: return - device = SatelIntegraAlarmPanel( - "Alarm Panel", - discovery_info.get(CONF_ARM_HOME_MODE), - discovery_info.get(CONF_DEVICE_PARTITION)) + configured_partitions = discovery_info[CONF_DEVICE_PARTITIONS] + controller = hass.data[DATA_SATEL] - async_add_entities([device]) + devices = [] + + for partition_num, device_config_data in configured_partitions.items(): + zone_name = device_config_data[CONF_ZONE_NAME] + arm_home_mode = device_config_data.get(CONF_ARM_HOME_MODE) + device = SatelIntegraAlarmPanel( + controller, + zone_name, + arm_home_mode, + partition_num) + devices.append(device) + + async_add_entities(devices) class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" - def __init__(self, name, arm_home_mode, partition_id): + def __init__(self, controller, name, arm_home_mode, partition_id): """Initialize the alarm panel.""" self._name = name self._state = None self._arm_home_mode = arm_home_mode self._partition_id = partition_id + self._satel = controller async def async_added_to_hass(self): """Update alarm status and register callbacks for future updates.""" @@ -66,13 +77,13 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): # Default - disarmed: hass_alarm_status = STATE_ALARM_DISARMED - satel_controller = self.hass.data[DATA_SATEL] - if not satel_controller.connected: + if not self._satel.connected: return None state_map = OrderedDict([ (AlarmState.TRIGGERED, STATE_ALARM_TRIGGERED), (AlarmState.TRIGGERED_FIRE, STATE_ALARM_TRIGGERED), + (AlarmState.ENTRY_TIME, STATE_ALARM_PENDING), (AlarmState.ARMED_MODE3, STATE_ALARM_ARMED_HOME), (AlarmState.ARMED_MODE2, STATE_ALARM_ARMED_HOME), (AlarmState.ARMED_MODE1, STATE_ALARM_ARMED_HOME), @@ -80,13 +91,11 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): (AlarmState.EXIT_COUNTDOWN_OVER_10, STATE_ALARM_PENDING), (AlarmState.EXIT_COUNTDOWN_UNDER_10, STATE_ALARM_PENDING) ]) - _LOGGER.debug("State map of Satel: %s", - satel_controller.partition_states) + _LOGGER.debug("State map of Satel: %s", self._satel.partition_states) for satel_state, ha_state in state_map.items(): - if satel_state in satel_controller.partition_states and\ - self._partition_id in\ - satel_controller.partition_states[satel_state]: + if satel_state in self._satel.partition_states and\ + self._partition_id in self._satel.partition_states[satel_state]: hass_alarm_status = ha_state break @@ -122,24 +131,24 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): _LOGGER.debug("Disarming, self._state: %s", self._state) - await self.hass.data[DATA_SATEL].disarm(code) + await self._satel.disarm(code, [self._partition_id]) if clear_alarm_necessary: # Wait 1s before clearing the alarm await asyncio.sleep(1) - await self.hass.data[DATA_SATEL].clear_alarm(code) + await self._satel.clear_alarm(code, [self._partition_id]) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" _LOGGER.debug("Arming away") if code: - await self.hass.data[DATA_SATEL].arm(code) + await self._satel.arm(code, [self._partition_id]) async def async_alarm_arm_home(self, code=None): """Send arm home command.""" _LOGGER.debug("Arming home") if code: - await self.hass.data[DATA_SATEL].arm( - code, self._arm_home_mode) + await self._satel.arm( + code, [self._partition_id], self._arm_home_mode) diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index faef1a6f45e..ebaf11f0766 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -6,8 +6,8 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - CONF_OUTPUTS, CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONES, DATA_SATEL, - SIGNAL_OUTPUTS_UPDATED, SIGNAL_ZONES_UPDATED) + CONF_OUTPUTS, CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONES, + SIGNAL_OUTPUTS_UPDATED, SIGNAL_ZONES_UPDATED, DATA_SATEL) _LOGGER = logging.getLogger(__name__) @@ -19,6 +19,7 @@ async def async_setup_platform( return configured_zones = discovery_info[CONF_ZONES] + controller = hass.data[DATA_SATEL] devices = [] @@ -26,7 +27,7 @@ async def async_setup_platform( zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] device = SatelIntegraBinarySensor( - zone_num, zone_name, zone_type, SIGNAL_ZONES_UPDATED) + controller, zone_num, zone_name, zone_type, SIGNAL_ZONES_UPDATED) devices.append(device) configured_outputs = discovery_info[CONF_OUTPUTS] @@ -35,7 +36,7 @@ async def async_setup_platform( zone_type = device_config_data[CONF_ZONE_TYPE] zone_name = device_config_data[CONF_ZONE_NAME] device = SatelIntegraBinarySensor( - zone_num, zone_name, zone_type, SIGNAL_OUTPUTS_UPDATED) + controller, zone_num, zone_name, zone_type, SIGNAL_OUTPUTS_UPDATED) devices.append(device) async_add_entities(devices) @@ -44,25 +45,25 @@ async def async_setup_platform( class SatelIntegraBinarySensor(BinarySensorDevice): """Representation of an Satel Integra binary sensor.""" - def __init__(self, device_number, device_name, zone_type, react_to_signal): + def __init__(self, controller, device_number, device_name, + zone_type, react_to_signal): """Initialize the binary_sensor.""" self._device_number = device_number self._name = device_name self._zone_type = zone_type self._state = 0 self._react_to_signal = react_to_signal + self._satel = controller async def async_added_to_hass(self): """Register callbacks.""" if self._react_to_signal == SIGNAL_OUTPUTS_UPDATED: - if self._device_number in\ - self.hass.data[DATA_SATEL].violated_outputs: + if self._device_number in self._satel.violated_outputs: self._state = 1 else: self._state = 0 else: - if self._device_number in\ - self.hass.data[DATA_SATEL].violated_zones: + if self._device_number in self._satel.violated_zones: self._state = 1 else: self._state = 0 diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 8df19ed90de..ae56b54ce18 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -3,7 +3,7 @@ "name": "Satel integra", "documentation": "https://www.home-assistant.io/components/satel_integra", "requirements": [ - "satel_integra==0.3.2" + "satel_integra==0.3.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py new file mode 100644 index 00000000000..77c07569fa4 --- /dev/null +++ b/homeassistant/components/satel_integra/switch.py @@ -0,0 +1,97 @@ +"""Support for Satel Integra modifiable outputs represented as switches.""" +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ( + CONF_DEVICE_CODE, CONF_SWITCHABLE_OUTPUTS, CONF_ZONE_NAME, + SIGNAL_OUTPUTS_UPDATED, DATA_SATEL) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['satel_integra'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the Satel Integra switch devices.""" + if not discovery_info: + return + + configured_zones = discovery_info[CONF_SWITCHABLE_OUTPUTS] + controller = hass.data[DATA_SATEL] + + devices = [] + + for zone_num, device_config_data in configured_zones.items(): + zone_name = device_config_data[CONF_ZONE_NAME] + + device = SatelIntegraSwitch( + controller, zone_num, zone_name, discovery_info[CONF_DEVICE_CODE]) + devices.append(device) + + async_add_entities(devices) + + +class SatelIntegraSwitch(SwitchDevice): + """Representation of an Satel switch.""" + + def __init__(self, controller, device_number, device_name, code): + """Initialize the binary_sensor.""" + self._device_number = device_number + self._name = device_name + self._state = False + self._code = code + self._satel = controller + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_OUTPUTS_UPDATED, self._devices_updated) + + @callback + def _devices_updated(self, zones): + """Update switch state, if needed.""" + _LOGGER.debug("Update switch name: %s zones: %s", self._name, zones) + if self._device_number in zones: + new_state = self._read_state() + _LOGGER.debug("New state: %s", new_state) + if new_state != self._state: + self._state = new_state + self.async_schedule_update_ha_state() + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + _LOGGER.debug("Switch: %s status: %s," + " turning on", self._name, self._state) + await self._satel.set_output(self._code, self._device_number, True) + self.async_schedule_update_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + _LOGGER.debug("Switch name: %s status: %s," + " turning off", self._name, self._state) + await self._satel.set_output(self._code, self._device_number, False) + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Return true if device is on.""" + self._state = self._read_state() + return self._state + + def _read_state(self): + """Read state of the device.""" + return self._device_number in self._satel.violated_outputs + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """Don't poll.""" + return False diff --git a/requirements_all.txt b/requirements_all.txt index 0a513debf9e..c37cceb1a12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1534,7 +1534,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.satel_integra -satel_integra==0.3.2 +satel_integra==0.3.4 # homeassistant.components.deutsche_bahn schiene==0.23