diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index bdbfa7bdf9c..183627fdfa6 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -39,6 +39,9 @@ CONF_ZONENAME = "name" CONF_ZONES = "zones" CONF_ZONETYPE = "type" +PANEL_TYPE_HONEYWELL = "HONEYWELL" +PANEL_TYPE_DSC = "DSC" + DEFAULT_PORT = 4025 DEFAULT_EVL_VERSION = 3 DEFAULT_KEEPALIVE = 60 @@ -50,6 +53,7 @@ DEFAULT_TIMEOUT = 10 SIGNAL_ZONE_UPDATE = "envisalink.zones_updated" SIGNAL_PARTITION_UPDATE = "envisalink.partition_updated" SIGNAL_KEYPAD_UPDATE = "envisalink.keypad_updated" +SIGNAL_ZONE_BYPASS_UPDATE = "envisalink.zone_bypass_updated" ZONE_SCHEMA = vol.Schema( { @@ -66,7 +70,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PANEL_TYPE): vol.All( - cv.string, vol.In(["HONEYWELL", "DSC"]) + cv.string, vol.In([PANEL_TYPE_HONEYWELL, PANEL_TYPE_DSC]) ), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASS): cv.string, @@ -137,14 +141,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DATA_EVL] = controller @callback - def login_fail_callback(data): + def async_login_fail_callback(data): """Handle when the evl rejects our login.""" _LOGGER.error("The Envisalink rejected your credentials") if not sync_connect.done(): sync_connect.set_result(False) @callback - def connection_fail_callback(data): + def async_connection_fail_callback(data): """Network failure callback.""" _LOGGER.error("Could not establish a connection with the Envisalink- retrying") if not sync_connect.done(): @@ -152,7 +156,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: sync_connect.set_result(True) @callback - def connection_success_callback(data): + def async_connection_success_callback(data): """Handle a successful connection.""" _LOGGER.info("Established a connection with the Envisalink") if not sync_connect.done(): @@ -160,23 +164,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: sync_connect.set_result(True) @callback - def zones_updated_callback(data): + def async_zones_updated_callback(data): """Handle zone timer updates.""" _LOGGER.debug("Envisalink sent a zone update event. Updating zones") async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data) @callback - def alarm_data_updated_callback(data): + def async_alarm_data_updated_callback(data): """Handle non-alarm based info updates.""" _LOGGER.debug("Envisalink sent new alarm info. Updating alarms") async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data) @callback - def partition_updated_callback(data): + def async_partition_updated_callback(data): """Handle partition changes thrown by evl (including alarms).""" _LOGGER.debug("The envisalink sent a partition update event") async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data) + @callback + def async_zone_bypass_update(data): + """Handle zone bypass status updates.""" + _LOGGER.debug("Envisalink sent a zone bypass update event. Updating zones") + async_dispatcher_send(hass, SIGNAL_ZONE_BYPASS_UPDATE, data) + @callback def stop_envisalink(event): """Shutdown envisalink connection and thread on exit.""" @@ -189,13 +199,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: partition = call.data.get(ATTR_PARTITION) controller.command_output(code, partition, custom_function) - controller.callback_zone_timer_dump = zones_updated_callback - controller.callback_zone_state_change = zones_updated_callback - controller.callback_partition_state_change = partition_updated_callback - controller.callback_keypad_update = alarm_data_updated_callback - controller.callback_login_failure = login_fail_callback - controller.callback_login_timeout = connection_fail_callback - controller.callback_login_success = connection_success_callback + controller.callback_zone_timer_dump = async_zones_updated_callback + controller.callback_zone_state_change = async_zones_updated_callback + controller.callback_partition_state_change = async_partition_updated_callback + controller.callback_keypad_update = async_alarm_data_updated_callback + controller.callback_login_failure = async_login_fail_callback + controller.callback_login_timeout = async_connection_fail_callback + controller.callback_login_success = async_connection_success_callback + controller.callback_zone_bypass_update = async_zone_bypass_update _LOGGER.info("Start envisalink") controller.start() @@ -229,6 +240,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config ) ) + # Only DSC panels support getting zone bypass status + if panel_type == PANEL_TYPE_DSC: + hass.async_create_task( + async_load_platform( + hass, "switch", "envisalink", {CONF_ZONES: zones}, config + ) + ) hass.services.async_register( DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index e6b54e586f8..f8341a82d82 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -68,39 +68,39 @@ async def async_setup_platform( code = discovery_info[CONF_CODE] panic_type = discovery_info[CONF_PANIC] - devices = [] + entities = [] for part_num in configured_partitions: - device_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) - device = EnvisalinkAlarm( + entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) + entity = EnvisalinkAlarm( hass, part_num, - device_config_data[CONF_PARTITIONNAME], + entity_config_data[CONF_PARTITIONNAME], code, panic_type, hass.data[DATA_EVL].alarm_state["partition"][part_num], hass.data[DATA_EVL], ) - devices.append(device) + entities.append(entity) - async_add_entities(devices) + async_add_entities(entities) @callback - def alarm_keypress_handler(service: ServiceCall) -> None: + def async_alarm_keypress_handler(service: ServiceCall) -> None: """Map services to methods on Alarm.""" entity_ids = service.data[ATTR_ENTITY_ID] keypress = service.data[ATTR_KEYPRESS] - target_devices = [ - device for device in devices if device.entity_id in entity_ids + target_entities = [ + entity for entity in entities if entity.entity_id in entity_ids ] - for device in target_devices: - device.async_alarm_keypress(keypress) + for entity in target_entities: + entity.async_alarm_keypress(keypress) hass.services.async_register( DOMAIN, SERVICE_ALARM_KEYPRESS, - alarm_keypress_handler, + async_alarm_keypress_handler, schema=ALARM_KEYPRESS_SCHEMA, ) @@ -123,17 +123,17 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( - self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback + self.hass, SIGNAL_KEYPAD_UPDATE, self.async_update_callback ) ) self.async_on_remove( async_dispatcher_connect( - self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback + self.hass, SIGNAL_PARTITION_UPDATE, self.async_update_callback ) ) @callback - def _update_callback(self, partition): + def async_update_callback(self, partition): """Update Home Assistant state, if needed.""" if partition is None or int(partition) == self._partition_number: self.async_write_ha_state() diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index 0c2a5ed325c..3fd7daeea86 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -30,25 +30,25 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Envisalink binary sensor devices.""" + """Set up the Envisalink binary sensor entities.""" if not discovery_info: return configured_zones = discovery_info["zones"] - devices = [] + entities = [] for zone_num in configured_zones: - device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) - device = EnvisalinkBinarySensor( + entity_config_data = ZONE_SCHEMA(configured_zones[zone_num]) + entity = EnvisalinkBinarySensor( hass, zone_num, - device_config_data[CONF_ZONENAME], - device_config_data[CONF_ZONETYPE], + entity_config_data[CONF_ZONENAME], + entity_config_data[CONF_ZONETYPE], hass.data[DATA_EVL].alarm_state["zone"][zone_num], hass.data[DATA_EVL], ) - devices.append(device) + entities.append(entity) - async_add_entities(devices) + async_add_entities(entities) class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): @@ -64,7 +64,11 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_UPDATE, self.async_update_callback + ) + ) @property def extra_state_attributes(self): @@ -102,7 +106,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): return self._zone_type @callback - def _update_callback(self, zone): + def async_update_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or int(zone) == self._zone_number: self.async_write_ha_state() diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 7ec8628be09..25290a5d431 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,7 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": ["pyenvisalink==4.0"], + "requirements": ["pyenvisalink==4.3"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index 51526cd000c..d8f31d7c4dd 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -27,25 +27,25 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Perform the setup for Envisalink sensor devices.""" + """Perform the setup for Envisalink sensor entities.""" if not discovery_info: return configured_partitions = discovery_info["partitions"] - devices = [] + entities = [] for part_num in configured_partitions: - device_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) - device = EnvisalinkSensor( + entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num]) + entity = EnvisalinkSensor( hass, - device_config_data[CONF_PARTITIONNAME], + entity_config_data[CONF_PARTITIONNAME], part_num, hass.data[DATA_EVL].alarm_state["partition"][part_num], hass.data[DATA_EVL], ) - devices.append(device) + entities.append(entity) - async_add_entities(devices) + async_add_entities(entities) class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): @@ -61,9 +61,15 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) - async_dispatcher_connect( - self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_KEYPAD_UPDATE, self.async_update_callback + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_PARTITION_UPDATE, self.async_update_callback + ) ) @property @@ -82,7 +88,7 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): return self._info["status"] @callback - def _update_callback(self, partition): + def async_update_callback(self, partition): """Update the partition state in HA, if needed.""" if partition is None or int(partition) == self._partition_number: self.async_write_ha_state() diff --git a/homeassistant/components/envisalink/switch.py b/homeassistant/components/envisalink/switch.py new file mode 100644 index 00000000000..6f5179a8649 --- /dev/null +++ b/homeassistant/components/envisalink/switch.py @@ -0,0 +1,87 @@ +"""Support for Envisalink zone bypass switches.""" +from __future__ import annotations + +import logging + +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import ( + CONF_ZONENAME, + DATA_EVL, + SIGNAL_ZONE_BYPASS_UPDATE, + ZONE_SCHEMA, + EnvisalinkDevice, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Envisalink switch entities.""" + if not discovery_info: + return + configured_zones = discovery_info["zones"] + + entities = [] + for zone_num in configured_zones: + entity_config_data = ZONE_SCHEMA(configured_zones[zone_num]) + zone_name = f"{entity_config_data[CONF_ZONENAME]}_bypass" + _LOGGER.debug("Setting up zone_bypass switch: %s", zone_name) + + entity = EnvisalinkSwitch( + hass, + zone_num, + zone_name, + hass.data[DATA_EVL].alarm_state["zone"][zone_num], + hass.data[DATA_EVL], + ) + entities.append(entity) + + async_add_entities(entities) + + +class EnvisalinkSwitch(EnvisalinkDevice, SwitchEntity): + """Representation of an Envisalink switch.""" + + def __init__(self, hass, zone_number, zone_name, info, controller): + """Initialize the switch.""" + self._zone_number = zone_number + + super().__init__(zone_name, info, controller) + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_BYPASS_UPDATE, self.async_update_callback + ) + ) + + @property + def is_on(self): + """Return the boolean response if the zone is bypassed.""" + return self._info["bypassed"] + + async def async_turn_on(self, **kwargs): + """Send the bypass keypress sequence to toggle the zone bypass.""" + self._controller.toggle_zone_bypass(self._zone_number) + + async def async_turn_off(self, **kwargs): + """Send the bypass keypress sequence to toggle the zone bypass.""" + self._controller.toggle_zone_bypass(self._zone_number) + + @callback + def async_update_callback(self, bypass_map): + """Update the zone bypass state in HA, if needed.""" + if bypass_map is None or self._zone_number in bypass_map: + _LOGGER.debug("Bypass state changed for zone %d", self._zone_number) + self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 46e91b1a2eb..db215aef1f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1500,7 +1500,7 @@ pyeight==0.2.0 pyemby==1.8 # homeassistant.components.envisalink -pyenvisalink==4.0 +pyenvisalink==4.3 # homeassistant.components.ephember pyephember==0.3.1