diff --git a/.gitignore b/.gitignore index 94f1119..f617c43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store .vscode +captured \ No newline at end of file diff --git a/README.md b/README.md index 2d357ee..911b2cd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,56 @@ # Home Assistant Ksenia Lares integration -[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) +[![GitHub Activity][commits-shield]][commits] +[![License][license-shield]](LICENSE) -Ksenia Lares (pre 4.0) integration for home assistant. This is integration is in early stages, use at own risk. +[![hacs][hacsbadge]][hacs] +![Project Maintenance][maintenance-shield] + +Ksenia Lares 48IP integration for home assistant. This is integration is in early stages, use at own risk. + +**This integration will set up the following platforms.** + +Platform | Description +-- | -- +`binary_sensor` | For each zone, defaults to movement sensor. +`sensor` | For each partition, showing the ARM status. +`alarm_panel` | ARM and disarm based on partitions and scenarios + +## Installation +### Installation via HACS +1. Open HACS +2. Goto the menu in to top right corner +3. Goto custom respositories +4. Add this repositry `https://github.com/johnnybegood/ha-ksenia-lares` +5. Click _explore & download respositories_ +6. Search for and add "Ksenia Lares" +7. Restart Home Assistant +8. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Ksenia Lares" or click the button below. + +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=ksenia_lares) + +### Installation directly + +1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). +2. If you do not have a `custom_components` directory (folder) there, you need to create it. +3. In the `custom_components` directory (folder) create a new folder called `ksenia_lares`. +4. Download _all_ the files from the `custom_components/ksenia_lares/` directory (folder) in this repository. +5. Place the files you downloaded in the new directory (folder) you created. +6. Restart Home Assistant +7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Ksenia Lares" or click the button below. + +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=ksenia_lares) + +## Quickstart +1. Check your Ksenia Lares has the web interface running +2. Enter the following information from Ksenia Lares 48IP web interface: + 1. Hostname or IP adress (port 4202 will be used) + 2. Username + 3. Password +3. To be able to use the alarm panel, click the 'configure' button on the Ksenia Lares [integration](https://my.home-assistant.io/redirect/integrations/) +4. Select the partitions and scenarios that match different ARM states. + 1. When the selected partitions are armed, the alarm panel will show the corresponding state. + 2. The selected scenario will be activated when changing arm state of the alarm panel ## WIP - [x] Detect zones from Lares alarm system and show as binary sensor diff --git a/custom_components/ksenia_lares/__init__.py b/custom_components/ksenia_lares/__init__.py index b675c84..e7c972d 100644 --- a/custom_components/ksenia_lares/__init__.py +++ b/custom_components/ksenia_lares/__init__.py @@ -5,13 +5,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.const import Platform from .base import LaresBase from .coordinator import LaresDataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, DATA_COORDINATOR, DATA_UPDATE_LISTENER CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.ALARM_CONTROL_PANEL] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -23,7 +24,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Preload device info await client.device_info() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + unsub_options_update_listener = entry.add_update_listener(options_update_listener) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_UPDATE_LISTENER: unsub_options_update_listener, + } hass.async_create_task( hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -32,6 +38,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True +async def options_update_listener(hass: HomeAssistant, config_entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( @@ -43,4 +54,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) + if unload_ok: + hass.data[DOMAIN][entry.entry_id][DATA_UPDATE_LISTENER]() + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/custom_components/ksenia_lares/alarm_control_panel.py b/custom_components/ksenia_lares/alarm_control_panel.py new file mode 100644 index 0000000..80eb583 --- /dev/null +++ b/custom_components/ksenia_lares/alarm_control_panel.py @@ -0,0 +1,221 @@ +"""This component provides support for Lares alarm control panel.""" +import logging +from datetime import timedelta + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, + CodeFormat, +) + +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMING, +) + +from .coordinator import LaresDataUpdateCoordinator +from .const import ( + DOMAIN, + DATA_COORDINATOR, + DATA_PARTITIONS, + PARTITION_STATUS_ARMED, + PARTITION_STATUS_ARMED_IMMEDIATE, + PARTITION_STATUS_ARMING, + CONF_PARTITION_AWAY, + CONF_PARTITION_HOME, + CONF_PARTITION_NIGHT, + CONF_SCENARIO_AWAY, + CONF_SCENARIO_HOME, + CONF_SCENARIO_NIGHT, + CONF_SCENARIO_DISARM, +) + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=10) + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up alarm control panel of the Lares alarm device from a config entry.""" + + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + device_info = await coordinator.client.device_info() + partition_descriptions = await coordinator.client.partition_descriptions() + scenario_descriptions = await coordinator.client.scenario_descriptions() + + options = { + CONF_PARTITION_AWAY: config_entry.options.get(CONF_PARTITION_AWAY, []), + CONF_PARTITION_HOME: config_entry.options.get(CONF_PARTITION_HOME, []), + CONF_PARTITION_NIGHT: config_entry.options.get(CONF_PARTITION_NIGHT, []), + CONF_SCENARIO_NIGHT: config_entry.options.get(CONF_SCENARIO_NIGHT, []), + CONF_SCENARIO_HOME: config_entry.options.get(CONF_SCENARIO_HOME, []), + CONF_SCENARIO_AWAY: config_entry.options.get(CONF_SCENARIO_AWAY, []), + CONF_SCENARIO_DISARM: config_entry.options.get(CONF_SCENARIO_DISARM, []), + } + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + async_add_devices( + [ + LaresAlarmControlPanel( + coordinator, + device_info, + partition_descriptions, + scenario_descriptions, + options, + ) + ] + ) + + +class LaresAlarmControlPanel(CoordinatorEntity, AlarmControlPanelEntity): + """An implementation of a Lares alarm control panel.""" + + TYPE = DOMAIN + ARMED_STATUS = [PARTITION_STATUS_ARMED, PARTITION_STATUS_ARMED_IMMEDIATE] + + def __init__( + self, + coordinator: LaresDataUpdateCoordinator, + device_info: dict, + partition_descriptions: dict, + scenario_descriptions: dict, + options: dict, + ) -> None: + """Initialize a the switch.""" + super().__init__(coordinator) + + self._coordinator = coordinator + self._partition_descriptions = partition_descriptions + self._scenario_descriptions = scenario_descriptions + self._options = options + self._attr_code_format = CodeFormat.NUMBER + self._attr_device_info = device_info + self._attr_code_arm_required = True + + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + name = self._attr_device_info["name"].replace(" ", "_") + return f"lares_panel_{name}" + + @property + def name(self): + """Return the name of this panel.""" + name = self._attr_device_info["name"] + return f"Panel {name}" + + @property + def supported_features(self) -> AlarmControlPanelEntityFeature: + """Return the list of supported features.""" + supported_features = AlarmControlPanelEntityFeature(0) + + if self._options[CONF_SCENARIO_AWAY] != "": + supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY + + if self._options[CONF_SCENARIO_HOME] != "": + supported_features |= AlarmControlPanelEntityFeature.ARM_HOME + + if self._options[CONF_SCENARIO_NIGHT] != "": + supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT + + return supported_features + + @property + def state(self) -> StateType: + """Return the state of this panel.""" + if self.__has_partition_with_status(PARTITION_STATUS_ARMING): + return STATE_ALARM_ARMING + + if self.__is_armed(CONF_PARTITION_AWAY): + return STATE_ALARM_ARMED_AWAY + + if self.__is_armed(CONF_PARTITION_HOME): + return STATE_ALARM_ARMED_HOME + + if self.__is_armed(CONF_PARTITION_NIGHT): + return STATE_ALARM_ARMED_NIGHT + + # If any of the not mapped partitions is armed, show custom as fallback + if self.__has_partition_with_status(self.ARMED_STATUS): + return STATE_ALARM_ARMED_CUSTOM_BYPASS + + return STATE_ALARM_DISARMED + + async def async_alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + await self.__command(CONF_SCENARIO_HOME, code) + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm home command.""" + await self.__command(CONF_SCENARIO_AWAY, code) + + async def async_alarm_arm_night(self, code: str | None = None) -> None: + """Send arm home command.""" + await self.__command(CONF_SCENARIO_NIGHT, code) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + await self.__command(CONF_SCENARIO_DISARM, code) + + def __has_partition_with_status(self, status_list: list[str]) -> bool: + """Return if any partitions is arming.""" + partitions = enumerate(self._coordinator.data[DATA_PARTITIONS]) + in_state = list( + idx for idx, partition in partitions if partition["status"] in status_list + ) + + _LOGGER.debug("%s in status %s", in_state, status_list) + + return len(in_state) > 0 + + def __is_armed(self, key: str) -> bool: + """Return if all partitions linked to the configuration key are armed.""" + partition_names = self._options[key] + + # Skip the check if no partitions are linked + if len(partition_names) == 0: + _LOGGER.debug("Skipping %s armed check, no definition", key) + return False + + descriptions = enumerate(self._partition_descriptions) + to_check = (idx for idx, name in descriptions if name in partition_names) + + _LOGGER.debug("Checking %s (%s) for %s", partition_names, to_check, key) + + for idx in to_check: + if ( + self._coordinator.data[DATA_PARTITIONS][idx]["status"] + not in self.ARMED_STATUS + ): + return False + + return True + + async def __command(self, key: str, code: str | None = None) -> None: + """Send arm home command.""" + scenario_name = self._options[key] + + if scenario_name is None: + _LOGGER.warning("Skipping command, no definition for %s", key) + return + + descriptions = enumerate(self._scenario_descriptions) + match_gen = (idx for idx, name in descriptions if name == scenario_name) + matches = list(match_gen) + + if len(matches) != 1: + _LOGGER.error("No match for %s (%s found)", key, len(matches)) + return + + scenario = matches[0] + _LOGGER.debug("Activating scenario %s", scenario) + + await self._coordinator.client.activate_scenario(scenario, code) diff --git a/custom_components/ksenia_lares/base.py b/custom_components/ksenia_lares/base.py index 2a05644..3d1acec 100644 --- a/custom_components/ksenia_lares/base.py +++ b/custom_components/ksenia_lares/base.py @@ -26,6 +26,7 @@ def __init__(self, data: dict) -> None: self._host = f"http://{host}:{self._port}" self._zone_descriptions = None self._partition_descriptions = None + self._scenario_descriptions = None async def info(self): """Get general info""" @@ -78,13 +79,9 @@ async def device_info(self): async def zone_descriptions(self): """Get available zones""" if self._zone_descriptions is None: - response = await self.get("zones/zonesDescription48IP.xml") - - if response is None: - return None - - zones = response.xpath("/zonesDescription/zone") - self._zone_descriptions = [zone.text for zone in zones] + self._zone_descriptions = await self.get_descriptions( + "zones/zonesDescription48IP.xml", "/zonesDescription/zone" + ) return self._zone_descriptions @@ -108,13 +105,10 @@ async def zones(self): async def partition_descriptions(self): """Get available partitions""" if self._partition_descriptions is None: - response = await self.get("partitions/partitionsDescription48IP.xml") - - if response is None: - return None - - partitions = response.xpath("/partitionsDescription/partition") - self._partition_descriptions = [partition.text for partition in partitions] + self._partition_descriptions = await self.get_descriptions( + "partitions/partitionsDescription48IP.xml", + "/partitionsDescription/partition", + ) return self._partition_descriptions @@ -134,6 +128,55 @@ async def paritions(self): for partition in partitions ] + async def scenarios(self): + """Get status of scenarios""" + response = await self.get("scenarios/scenariosOptions.xml") + + if response is None: + return None + + scenarios = response.xpath("/scenariosOptions/scenario") + + return [ + { + "id": idx, + "enabled": scenario.find("abil").text == "TRUE", + "noPin": scenario.find("nopin").text == "TRUE", + } + for idx, scenario in enumerate(scenarios) + ] + + async def scenario_descriptions(self): + """Get descriptions of scenarios""" + if self._scenario_descriptions is None: + self._scenario_descriptions = await self.get_descriptions( + "scenarios/scenariosDescription.xml", "/scenariosDescription/scenario" + ) + + return self._scenario_descriptions + + async def activate_scenario(self, scenario: int, code: str) -> bool: + """Activate the given scenarios, requires the alarm code""" + path = f"cmd/cmdOk.xml?cmd=setMacro&pin={code}¯oId={scenario}&redirectPage=/xml/cmd/cmdError.xml" + response = await self.get(path) + cmd = response.xpath("/cmd") + + if cmd is None or cmd[0].text != "cmdSent": + _LOGGER.error("Active scenario failed: %s", response) + return False + + return True + + async def get_descriptions(self, path: str, element: str) -> dict: + """Get descriptions""" + response = await self.get(path) + + if response is None: + return None + + content = response.xpath(element) + return [item.text for item in content] + async def get(self, path): """Generic send method.""" url = f"{self._host}/xml/{path}" @@ -149,3 +192,4 @@ async def get(self, path): _LOGGER.debug("Host %s: Connection error %s", self._host, str(conn_err)) except: # pylint: disable=bare-except _LOGGER.debug("Host %s: Unknown exception occurred", self._host) + return diff --git a/custom_components/ksenia_lares/binary_sensor.py b/custom_components/ksenia_lares/binary_sensor.py index 82833f2..dbda1db 100644 --- a/custom_components/ksenia_lares/binary_sensor.py +++ b/custom_components/ksenia_lares/binary_sensor.py @@ -18,6 +18,7 @@ ZONE_BYPASS_ON, ZONE_STATUS_ALARM, ZONE_STATUS_NOT_USED, + DATA_COORDINATOR, ) _LOGGER = logging.getLogger(__name__) @@ -34,7 +35,7 @@ async def async_setup_entry( ) -> None: """Set up binary sensors attached to a Lares alarm device from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] device_info = await coordinator.client.device_info() zone_descriptions = await coordinator.client.zone_descriptions() diff --git a/custom_components/ksenia_lares/config_flow.py b/custom_components/ksenia_lares/config_flow.py index 22575a0..63ad010 100644 --- a/custom_components/ksenia_lares/config_flow.py +++ b/custom_components/ksenia_lares/config_flow.py @@ -1,12 +1,30 @@ """Config flow for Ksenia Lares Alarm integration.""" import logging +from typing import Any import voluptuous as vol - -from homeassistant import config_entries, core, exceptions +import homeassistant.helpers.config_validation as cv + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + OptionsFlow, + FlowResult, +) +from homeassistant.core import callback, HomeAssistant from .base import LaresBase -from .const import DOMAIN +from .const import ( + DOMAIN, + CONF_PARTITION_AWAY, + CONF_PARTITION_HOME, + CONF_PARTITION_NIGHT, + CONF_SCENARIO_HOME, + CONF_SCENARIO_AWAY, + CONF_SCENARIO_NIGHT, + CONF_SCENARIO_DISARM, +) _LOGGER = logging.getLogger(__name__) @@ -19,7 +37,7 @@ ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. @@ -35,11 +53,19 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": info["name"], "id": info["id"]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LaresConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ksenia Lares Alarm.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Return the options flow.""" + return LaresOptionsFlowHandler(config_entry) + async def async_step_user(self, user_input=None): """Handle the initial step.""" if user_input is None: @@ -70,9 +96,64 @@ async def async_step_user(self, user_input=None): ) -class CannotConnect(exceptions.HomeAssistantError): +class LaresOptionsFlowHandler(OptionsFlow): + """Handle a options flow for Ksenia Lares Alarm.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + self.client = LaresBase(config_entry.data) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + partitions = await self.client.partition_descriptions() + select_partitions = {v: v for v in list(filter(None, partitions)) if v != ""} + + scenarios = await self.client.scenario_descriptions() + scenarios_with_empty = [""] + scenarios + + options = { + vol.Required( + CONF_SCENARIO_DISARM, + default=self.config_entry.options.get(CONF_SCENARIO_DISARM, ""), + ): vol.In(scenarios), + vol.Required( + CONF_PARTITION_AWAY, + default=self.config_entry.options.get(CONF_PARTITION_AWAY, []), + ): cv.multi_select(select_partitions), + vol.Required( + CONF_SCENARIO_AWAY, + default=self.config_entry.options.get(CONF_SCENARIO_AWAY, ""), + ): vol.In(scenarios), + vol.Optional( + CONF_PARTITION_HOME, + default=self.config_entry.options.get(CONF_PARTITION_HOME, []), + ): cv.multi_select(select_partitions), + vol.Optional( + CONF_SCENARIO_HOME, + default=self.config_entry.options.get(CONF_SCENARIO_HOME, ""), + ): vol.In(scenarios_with_empty), + vol.Optional( + CONF_PARTITION_NIGHT, + default=self.config_entry.options.get(CONF_PARTITION_NIGHT, []), + ): cv.multi_select(select_partitions), + vol.Optional( + CONF_SCENARIO_NIGHT, + default=self.config_entry.options.get(CONF_SCENARIO_NIGHT, ""), + ): vol.In(scenarios_with_empty), + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/custom_components/ksenia_lares/const.py b/custom_components/ksenia_lares/const.py index dd4817d..5d34a81 100644 --- a/custom_components/ksenia_lares/const.py +++ b/custom_components/ksenia_lares/const.py @@ -14,4 +14,21 @@ ZONE_BYPASS_OFF = "UN_BYPASS" ZONE_BYPASS_ON = "BYPASS" -DISARMED_STATE = "DISARMED" +PARTITION_STATUS_DISARMED = "DISARMED" +PARTITION_STATUS_ARMED = "ARMED" +PARTITION_STATUS_ARMED_IMMEDIATE = "ARMED_IMMEDIATE" +PARTITION_STATUS_ARMING = "EXIT" +PARTITION_STATUS_PENDING = "PREALARM" +PARTITION_STATUS_ALARM = "ALARM" + +CONF_PARTITION_AWAY = "partition_away" +CONF_PARTITION_HOME = "partition_home" +CONF_PARTITION_NIGHT = "partition_night" + +CONF_SCENARIO_AWAY = "scenario_away" +CONF_SCENARIO_HOME = "scenario_home" +CONF_SCENARIO_NIGHT = "scenario_night" +CONF_SCENARIO_DISARM = "scenario_disarm" + +DATA_COORDINATOR = "coordinator" +DATA_UPDATE_LISTENER = "update_listener" diff --git a/custom_components/ksenia_lares/manifest.json b/custom_components/ksenia_lares/manifest.json index a1a6ab3..4969419 100644 --- a/custom_components/ksenia_lares/manifest.json +++ b/custom_components/ksenia_lares/manifest.json @@ -1,18 +1,19 @@ { "domain": "ksenia_lares", "name": "Ksenia Lares Alarm", - "version": "1.1.0", + "codeowners": [ + "@johnnybegood" + ], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/ksenia_lares", + "dependencies": [], + "documentation": "https://github.com/johnnybegood/ha-ksenia-lares", + "homekit": {}, + "iot_class": "local_polling", "requirements": [ "lxml==4.9.1", "getmac==0.8.2" ], "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], - "codeowners": [ - "@johnnybegood" - ] -} + "version": "1.0.2", + "zeroconf": [] +} \ No newline at end of file diff --git a/custom_components/ksenia_lares/sensor.py b/custom_components/ksenia_lares/sensor.py index 2fb514a..03a0cb8 100644 --- a/custom_components/ksenia_lares/sensor.py +++ b/custom_components/ksenia_lares/sensor.py @@ -1,9 +1,7 @@ """This component provides support for Lares partitions.""" from datetime import timedelta -import logging - -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorDeviceClass from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) @@ -12,12 +10,19 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, DATA_PARTITIONS, ZONE_STATUS_NOT_USED - -_LOGGER = logging.getLogger(__name__) +from .const import ( + DOMAIN, + DATA_PARTITIONS, + PARTITION_STATUS_DISARMED, + PARTITION_STATUS_ARMED, + PARTITION_STATUS_ARMED_IMMEDIATE, + PARTITION_STATUS_ARMING, + PARTITION_STATUS_PENDING, + PARTITION_STATUS_ALARM, + DATA_COORDINATOR, +) SCAN_INTERVAL = timedelta(seconds=10) - DEFAULT_DEVICE_CLASS = "motion" @@ -28,7 +33,7 @@ async def async_setup_entry( ) -> None: """Set up sensors attached to a Lares alarm device from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] device_info = await coordinator.client.device_info() partition_descriptions = await coordinator.client.partition_descriptions() @@ -52,7 +57,17 @@ def __init__(self, coordinator, idx, description, device_info) -> None: self._description = description self._idx = idx + self._attr_icon = "mdi:shield" self._attr_device_info = device_info + self._attr_device_class = SensorDeviceClass.ENUM + self._attr_options = [ + PARTITION_STATUS_DISARMED, + PARTITION_STATUS_ARMED, + PARTITION_STATUS_ARMED_IMMEDIATE, + PARTITION_STATUS_ARMING, + PARTITION_STATUS_PENDING, + PARTITION_STATUS_ALARM, + ] # Hide sensor if it has no description is_inactive = not self._description diff --git a/custom_components/ksenia_lares/strings.json b/custom_components/ksenia_lares/strings.json index 365dd30..8ba397f 100644 --- a/custom_components/ksenia_lares/strings.json +++ b/custom_components/ksenia_lares/strings.json @@ -1,5 +1,4 @@ { - "title": "Ksenia Lares Alarm", "config": { "step": { "user": { @@ -18,5 +17,30 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "description": "Link different alarm states to partition and scenarios.", + "data": { + "scenario_disarm": "Disarm scenario", + "partition_away": "Away paritions", + "scenario_away": "Away scenario", + "partition_home": "Home partitions", + "scenario_home": "Home scenario", + "partition_night": "Night partitions", + "scenario_night": "Night scenario" + }, + "data_description": { + "scenario_disarm": "Select the scenario to activate to disarm", + "partition_away": "Select all partitions that need to armed for away state", + "scenario_away": "Select the scenario to activate to arm away", + "partition_home": "Select all partitions that need to armed for home state", + "scenario_home": "Select the scenario to activate to arm home", + "partition_night": "Select all partitions that need to armed for night state", + "scenario_night": "Select the scenario to activate to arm night" + } + } + } } } \ No newline at end of file diff --git a/custom_components/ksenia_lares/translations/en.json b/custom_components/ksenia_lares/translations/en.json index a3f8335..d9f3a55 100644 --- a/custom_components/ksenia_lares/translations/en.json +++ b/custom_components/ksenia_lares/translations/en.json @@ -18,5 +18,29 @@ } } }, - "title": "Ksenia Lares Alarm" + "options": { + "step": { + "init": { + "description": "Link partitions to states, the state will be set when the selected partitions are armed", + "data": { + "scenario_disarm": "Disarm scenario", + "partition_away": "Away paritions", + "scenario_away": "Away scenario", + "partition_home": "Home partitions", + "scenario_home": "Home scenario", + "partition_night": "Night partitions", + "scenario_night": "Night scenario" + }, + "data_description": { + "scenario_disarm": "Select the scenario to activate to disarm", + "partition_away": "Select all partitions that need to armed for away state", + "scenario_away": "Select the scenario to activate to arm away", + "partition_home": "Select all partitions that need to armed for home state", + "scenario_home": "Select the scenario to activate to arm home", + "partition_night": "Select all partitions that need to armed for night state", + "scenario_night": "Select the scenario to activate to arm night" + } + } + } + } } \ No newline at end of file