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
This commit is contained in:
c-soft 2019-04-13 14:24:12 +02:00 committed by Martin Hjelmare
parent 18cf8275b8
commit 2f17529f28
6 changed files with 186 additions and 44 deletions

View File

@ -1,9 +1,10 @@
"""Support for Satel Integra devices.""" """Support for Satel Integra devices."""
import collections
import logging import logging
import voluptuous as vol 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.core import callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
@ -21,13 +22,14 @@ DOMAIN = 'satel_integra'
DATA_SATEL = 'satel_integra' DATA_SATEL = 'satel_integra'
CONF_DEVICE_PORT = 'port' CONF_DEVICE_CODE = 'code'
CONF_DEVICE_PARTITION = 'partition' CONF_DEVICE_PARTITIONS = 'partitions'
CONF_ARM_HOME_MODE = 'arm_home_mode' CONF_ARM_HOME_MODE = 'arm_home_mode'
CONF_ZONE_NAME = 'name' CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type' CONF_ZONE_TYPE = 'type'
CONF_ZONES = 'zones' CONF_ZONES = 'zones'
CONF_OUTPUTS = 'outputs' CONF_OUTPUTS = 'outputs'
CONF_SWITCHABLE_OUTPUTS = 'switchable_outputs'
ZONES = 'zones' ZONES = 'zones'
@ -42,20 +44,38 @@ SIGNAL_OUTPUTS_UPDATED = 'satel_integra.outputs_updated'
ZONE_SCHEMA = vol.Schema({ ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_NAME): cv.string, vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): 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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.All({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_DEVICE_PARTITION, vol.Optional(CONF_DEVICE_CODE): cv.string,
default=DEFAULT_DEVICE_PARTITION): cv.positive_int, vol.Optional(CONF_DEVICE_PARTITIONS,
vol.Optional(CONF_ARM_HOME_MODE, default={}): {vol.Coerce(int): PARTITION_SCHEMA},
default=DEFAULT_CONF_ARM_HOME_MODE): vol.In([1, 2, 3]),
vol.Optional(CONF_ZONES, vol.Optional(CONF_ZONES,
default={}): {vol.Coerce(int): ZONE_SCHEMA}, default={}): {vol.Coerce(int): ZONE_SCHEMA},
vol.Optional(CONF_OUTPUTS, vol.Optional(CONF_OUTPUTS,
default={}): {vol.Coerce(int): ZONE_SCHEMA}, 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) }, extra=vol.ALLOW_EXTRA)
@ -65,13 +85,20 @@ async def async_setup(hass, config):
zones = conf.get(CONF_ZONES) zones = conf.get(CONF_ZONES)
outputs = conf.get(CONF_OUTPUTS) outputs = conf.get(CONF_OUTPUTS)
switchable_outputs = conf.get(CONF_SWITCHABLE_OUTPUTS)
host = conf.get(CONF_HOST) host = conf.get(CONF_HOST)
port = conf.get(CONF_DEVICE_PORT) port = conf.get(CONF_PORT)
partition = conf.get(CONF_DEVICE_PARTITION) partitions = conf.get(CONF_DEVICE_PARTITIONS)
from satel_integra.satel_integra import AsyncSatel 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 hass.data[DATA_SATEL] = controller
@ -94,7 +121,15 @@ async def async_setup(hass, config):
hass.async_create_task( hass.async_create_task(
async_load_platform(hass, 'binary_sensor', DOMAIN, 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 @callback

View File

@ -11,7 +11,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import ( 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) SIGNAL_PANEL_MESSAGE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,23 +23,34 @@ async def async_setup_platform(
if not discovery_info: if not discovery_info:
return return
device = SatelIntegraAlarmPanel( configured_partitions = discovery_info[CONF_DEVICE_PARTITIONS]
"Alarm Panel", controller = hass.data[DATA_SATEL]
discovery_info.get(CONF_ARM_HOME_MODE),
discovery_info.get(CONF_DEVICE_PARTITION))
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): class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an AlarmDecoder-based alarm panel.""" """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.""" """Initialize the alarm panel."""
self._name = name self._name = name
self._state = None self._state = None
self._arm_home_mode = arm_home_mode self._arm_home_mode = arm_home_mode
self._partition_id = partition_id self._partition_id = partition_id
self._satel = controller
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Update alarm status and register callbacks for future updates.""" """Update alarm status and register callbacks for future updates."""
@ -66,13 +77,13 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
# Default - disarmed: # Default - disarmed:
hass_alarm_status = STATE_ALARM_DISARMED hass_alarm_status = STATE_ALARM_DISARMED
satel_controller = self.hass.data[DATA_SATEL] if not self._satel.connected:
if not satel_controller.connected:
return None return None
state_map = OrderedDict([ state_map = OrderedDict([
(AlarmState.TRIGGERED, STATE_ALARM_TRIGGERED), (AlarmState.TRIGGERED, STATE_ALARM_TRIGGERED),
(AlarmState.TRIGGERED_FIRE, STATE_ALARM_TRIGGERED), (AlarmState.TRIGGERED_FIRE, STATE_ALARM_TRIGGERED),
(AlarmState.ENTRY_TIME, STATE_ALARM_PENDING),
(AlarmState.ARMED_MODE3, STATE_ALARM_ARMED_HOME), (AlarmState.ARMED_MODE3, STATE_ALARM_ARMED_HOME),
(AlarmState.ARMED_MODE2, STATE_ALARM_ARMED_HOME), (AlarmState.ARMED_MODE2, STATE_ALARM_ARMED_HOME),
(AlarmState.ARMED_MODE1, 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_OVER_10, STATE_ALARM_PENDING),
(AlarmState.EXIT_COUNTDOWN_UNDER_10, STATE_ALARM_PENDING) (AlarmState.EXIT_COUNTDOWN_UNDER_10, STATE_ALARM_PENDING)
]) ])
_LOGGER.debug("State map of Satel: %s", _LOGGER.debug("State map of Satel: %s", self._satel.partition_states)
satel_controller.partition_states)
for satel_state, ha_state in state_map.items(): for satel_state, ha_state in state_map.items():
if satel_state in satel_controller.partition_states and\ if satel_state in self._satel.partition_states and\
self._partition_id in\ self._partition_id in self._satel.partition_states[satel_state]:
satel_controller.partition_states[satel_state]:
hass_alarm_status = ha_state hass_alarm_status = ha_state
break break
@ -122,24 +131,24 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
_LOGGER.debug("Disarming, self._state: %s", self._state) _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: if clear_alarm_necessary:
# Wait 1s before clearing the alarm # Wait 1s before clearing the alarm
await asyncio.sleep(1) 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): async def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
_LOGGER.debug("Arming away") _LOGGER.debug("Arming away")
if code: 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): async def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
_LOGGER.debug("Arming home") _LOGGER.debug("Arming home")
if code: if code:
await self.hass.data[DATA_SATEL].arm( await self._satel.arm(
code, self._arm_home_mode) code, [self._partition_id], self._arm_home_mode)

View File

@ -6,8 +6,8 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import ( from . import (
CONF_OUTPUTS, CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONES, DATA_SATEL, CONF_OUTPUTS, CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONES,
SIGNAL_OUTPUTS_UPDATED, SIGNAL_ZONES_UPDATED) SIGNAL_OUTPUTS_UPDATED, SIGNAL_ZONES_UPDATED, DATA_SATEL)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -19,6 +19,7 @@ async def async_setup_platform(
return return
configured_zones = discovery_info[CONF_ZONES] configured_zones = discovery_info[CONF_ZONES]
controller = hass.data[DATA_SATEL]
devices = [] devices = []
@ -26,7 +27,7 @@ async def async_setup_platform(
zone_type = device_config_data[CONF_ZONE_TYPE] zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME] zone_name = device_config_data[CONF_ZONE_NAME]
device = SatelIntegraBinarySensor( device = SatelIntegraBinarySensor(
zone_num, zone_name, zone_type, SIGNAL_ZONES_UPDATED) controller, zone_num, zone_name, zone_type, SIGNAL_ZONES_UPDATED)
devices.append(device) devices.append(device)
configured_outputs = discovery_info[CONF_OUTPUTS] configured_outputs = discovery_info[CONF_OUTPUTS]
@ -35,7 +36,7 @@ async def async_setup_platform(
zone_type = device_config_data[CONF_ZONE_TYPE] zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME] zone_name = device_config_data[CONF_ZONE_NAME]
device = SatelIntegraBinarySensor( device = SatelIntegraBinarySensor(
zone_num, zone_name, zone_type, SIGNAL_OUTPUTS_UPDATED) controller, zone_num, zone_name, zone_type, SIGNAL_OUTPUTS_UPDATED)
devices.append(device) devices.append(device)
async_add_entities(devices) async_add_entities(devices)
@ -44,25 +45,25 @@ async def async_setup_platform(
class SatelIntegraBinarySensor(BinarySensorDevice): class SatelIntegraBinarySensor(BinarySensorDevice):
"""Representation of an Satel Integra binary sensor.""" """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.""" """Initialize the binary_sensor."""
self._device_number = device_number self._device_number = device_number
self._name = device_name self._name = device_name
self._zone_type = zone_type self._zone_type = zone_type
self._state = 0 self._state = 0
self._react_to_signal = react_to_signal self._react_to_signal = react_to_signal
self._satel = controller
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
if self._react_to_signal == SIGNAL_OUTPUTS_UPDATED: if self._react_to_signal == SIGNAL_OUTPUTS_UPDATED:
if self._device_number in\ if self._device_number in self._satel.violated_outputs:
self.hass.data[DATA_SATEL].violated_outputs:
self._state = 1 self._state = 1
else: else:
self._state = 0 self._state = 0
else: else:
if self._device_number in\ if self._device_number in self._satel.violated_zones:
self.hass.data[DATA_SATEL].violated_zones:
self._state = 1 self._state = 1
else: else:
self._state = 0 self._state = 0

View File

@ -3,7 +3,7 @@
"name": "Satel integra", "name": "Satel integra",
"documentation": "https://www.home-assistant.io/components/satel_integra", "documentation": "https://www.home-assistant.io/components/satel_integra",
"requirements": [ "requirements": [
"satel_integra==0.3.2" "satel_integra==0.3.4"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []

View File

@ -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

View File

@ -1534,7 +1534,7 @@ rxv==0.6.0
samsungctl[websocket]==0.7.1 samsungctl[websocket]==0.7.1
# homeassistant.components.satel_integra # homeassistant.components.satel_integra
satel_integra==0.3.2 satel_integra==0.3.4
# homeassistant.components.deutsche_bahn # homeassistant.components.deutsche_bahn
schiene==0.23 schiene==0.23