diff --git a/README.md b/README.md index c03de598..41f78f8f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ This integration supports config flow, so go to the list of integrations and cli - Enter the username and password for your Securitas account. - Use 2FA (default: yes). Uncheck this box if you want to skip the 2FA. - Country Code. One of BR (Brasil), CL (Chile), ES (Spain), FR (France), GB (Great Britain), IE (Ireland), IT (Italy) and AR (Argentine). If you are outside of those countries, try entering "default" and if that doesn't work open an issue to see if we can expand. -- PIN code (optional). If you set a PIN here, you will need to enter it to arm or disarm the alarm using the Home Assistant panel. This PIN is independent of Securitas. It is never sent to Securitas and it has nothing to do with your account with them. +- PIN code (optional). If you set a PIN here, you will need to enter it to disarm the alarm using the Home Assistant panel. This PIN is independent of Securitas. It is never sent to Securitas and it has nothing to do with your account with them. The PIN can be numeric or alphanumeric -- the input format adapts automatically. +- Require PIN code to arm (default: no). If enabled, the PIN code is also required when arming the alarm (Home, Away, Night, Custom). When disabled, only disarming requires the PIN. This is useful for Android Auto and other interfaces where entering a PIN to arm may not be practical. - Perimetral alarm (default: no). If you have sensors outside of your home, check the box. Otherwise, leave the box unchecked. This will ensure that the integration sends the correct commands to arm the alarm. - Check alarm panel (default: yes). The integration checks periodically the status of the alarm (see next option). If this Option is On, the integration will check the alarm status in the alarm in your home and HA will reflect the alarm's status. This will result in the requests showing in your account and there are reports of users saying that Securitas calls them to ask about these requests. If this option is Off, the integration will check the last status that Securitas have in their server instead of checking in the alarm itself. This will decrease the number of request that show in your account. But if you arm or disarm the alarm using the Securitas app, the alarm in Home Assistant will likely show a different state. - Update scan interval (default: 120). How often the integration checks the status of the alarm. diff --git a/custom_components/securitas/__init__.py b/custom_components/securitas/__init__.py index f6565f20..756f5dc0 100644 --- a/custom_components/securitas/__init__.py +++ b/custom_components/securitas/__init__.py @@ -46,6 +46,7 @@ DOMAIN = "securitas" CONF_COUNTRY = "country" +CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CHECK_ALARM_PANEL = "check_alarm_panel" CONF_USE_2FA = "use_2FA" CONF_PERI_ALARM = "PERI_alarm" @@ -60,10 +61,12 @@ DEFAULT_USE_2FA = True DEFAULT_SCAN_INTERVAL = 120 +DEFAULT_CODE_ARM_REQUIRED = False DEFAULT_CHECK_ALARM_PANEL = True DEFAULT_DELAY_CHECK_OPERATION = 2 DEFAULT_CODE = "" DEFAULT_PERI_ALARM = False +DEFAULT_COUNTRY = "ES" PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR, Platform.LOCK] @@ -77,9 +80,10 @@ vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_USE_2FA, default=DEFAULT_USE_2FA): bool, - vol.Optional(CONF_COUNTRY, default="ES"): str, + vol.Optional(CONF_COUNTRY, default=DEFAULT_COUNTRY): str, vol.Optional(CONF_CODE, default=DEFAULT_CODE): str, vol.Optional(CONF_PERI_ALARM, default=DEFAULT_PERI_ALARM): bool, + vol.Optional(CONF_CODE_ARM_REQUIRED, default=DEFAULT_CODE_ARM_REQUIRED): bool, vol.Optional( CONF_CHECK_ALARM_PANEL, default=DEFAULT_CHECK_ALARM_PANEL ): bool, @@ -111,6 +115,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: entry.data.get(attrib) != entry.options.get(attrib) for attrib in ( CONF_CODE, + CONF_CODE_ARM_REQUIRED, CONF_SCAN_INTERVAL, CONF_CHECK_ALARM_PANEL, CONF_PERI_ALARM, @@ -138,6 +143,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config[CONF_COUNTRY] = entry.data.get(CONF_COUNTRY, None) config[CONF_CODE] = entry.data.get(CONF_CODE, DEFAULT_CODE) config[CONF_PERI_ALARM] = entry.data.get(CONF_PERI_ALARM, DEFAULT_PERI_ALARM) + config[CONF_CODE_ARM_REQUIRED] = entry.data.get( + CONF_CODE_ARM_REQUIRED, DEFAULT_CODE_ARM_REQUIRED + ) config[CONF_CHECK_ALARM_PANEL] = entry.data.get( CONF_CHECK_ALARM_PANEL, DEFAULT_CHECK_ALARM_PANEL ) diff --git a/custom_components/securitas/alarm_control_panel.py b/custom_components/securitas/alarm_control_panel.py index f609b9b7..ac6b8ed4 100644 --- a/custom_components/securitas/alarm_control_panel.py +++ b/custom_components/securitas/alarm_control_panel.py @@ -18,10 +18,12 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval +from homeassistant.exceptions import ServiceValidationError from . import ( CONF_INSTALLATION_KEY, DEFAULT_SCAN_INTERVAL, + CONF_CODE_ARM_REQUIRED, DOMAIN, CONF_PERI_ALARM, DEFAULT_PERI_ALARM, @@ -125,6 +127,11 @@ def __init__( self._update_unsub = async_track_time_interval( hass, self.async_update_status, self._update_interval ) + self._code: str | None = client.config.get(CONF_CODE, None) + self._attr_code_format: CodeFormat | None = None + if self._code: + self._attr_code_format = CodeFormat.NUMBER if self._code.isdigit() else CodeFormat.TEXT + self._attr_code_arm_required: bool = client.config.get(CONF_CODE_ARM_REQUIRED, True) if self._code else False self._attr_device_info: DeviceInfo = DeviceInfo( identifiers={(DOMAIN, self._attr_unique_id)}, @@ -159,21 +166,6 @@ def name(self) -> str: """Return the name of the device.""" return self.installation.alias - @property - def code_format(self) -> CodeFormat: - """Return one or more digits/characters.""" - return CodeFormat.NUMBER - - @property - def code_arm_required(self) -> bool: - """Whether the code is required for arm actions.""" - return False - - @property - def changed_by(self) -> str: - """Return the last change triggered by.""" - return self._changed_by - async def get_arm_state(self) -> CheckAlarmStatus: """Get alarm state.""" reference_id: str = await self.client.session.check_alarm(self.installation) @@ -237,25 +229,28 @@ def update_status_alarm(self, status: CheckAlarmStatus | None = None) -> None: f"Securitas Direct integration options.", ) - def check_code(self, code=None) -> bool: - """Check that the code entered in the panel matches the code in the config.""" - - result: bool = False - - if ( - self.client.config.get(CONF_CODE, "") == "" - or str(self.client.config.get(CONF_CODE, "")) == str(code) - or self.client.config.get(CONF_CODE, None) is None - ): - result = True - else: - _LOGGER.info("PIN doesn't match") + def _check_code_for_arm_if_required(self, code: str | None) -> bool: + """Check the code only if arming requires a code.""" + if not self.code_arm_required: + return True + return self._check_code(code) + def _check_code(self, code: str | None) -> bool: + """Check that the code entered in the panel matches the code in the config.""" + result: bool = not self._code or self._code == code + if not result: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_pin_code", + translation_placeholders={ + "entity_id": self.entity_id, + }, + ) return result async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - if self.check_code(code): + if self._check_code(code): self.__force_state(AlarmControlPanelState.DISARMING) disarm_status: DisarmStatus = DisarmStatus() try: @@ -337,25 +332,25 @@ async def set_arm_state(self, mode: str) -> None: async def async_alarm_arm_home(self, code: str | None = None): """Send arm home command.""" - if self.check_code(code): + if self._check_code_for_arm_if_required(code): self.__force_state(AlarmControlPanelState.ARMING) await self.set_arm_state(AlarmControlPanelState.ARMED_HOME) async def async_alarm_arm_away(self, code: str | None = None): """Send arm away command.""" - if self.check_code(code): + if self._check_code_for_arm_if_required(code): self.__force_state(AlarmControlPanelState.ARMING) await self.set_arm_state(AlarmControlPanelState.ARMED_AWAY) async def async_alarm_arm_night(self, code: str | None = None): """Send arm home command.""" - if self.check_code(code): + if self._check_code_for_arm_if_required(code): self.__force_state(AlarmControlPanelState.ARMING) await self.set_arm_state(AlarmControlPanelState.ARMED_NIGHT) async def async_alarm_arm_custom_bypass(self, code: str | None = None): """Send arm perimeter command.""" - if self.check_code(code): + if self._check_code_for_arm_if_required(code): self.__force_state(AlarmControlPanelState.ARMING) await self.set_arm_state(AlarmControlPanelState.ARMED_CUSTOM_BYPASS) diff --git a/custom_components/securitas/config_flow.py b/custom_components/securitas/config_flow.py index 9ff6128b..b843e7d3 100644 --- a/custom_components/securitas/config_flow.py +++ b/custom_components/securitas/config_flow.py @@ -25,6 +25,7 @@ from homeassistant.helpers.selector import selector from . import ( + CONF_CODE_ARM_REQUIRED, CONF_CHECK_ALARM_PANEL, CONF_COUNTRY, CONF_DELAY_CHECK_OPERATION, @@ -37,10 +38,12 @@ CONF_PERI_ALARM, CONF_USE_2FA, CONFIG_SCHEMA, + DEFAULT_CODE_ARM_REQUIRED, DEFAULT_CHECK_ALARM_PANEL, DEFAULT_DELAY_CHECK_OPERATION, DEFAULT_PERI_ALARM, DEFAULT_SCAN_INTERVAL, + DEFAULT_CODE, DOMAIN, SecuritasDirectDevice, SecuritasHub, @@ -216,9 +219,18 @@ async def async_step_import(self, user_input: dict): self.config[CONF_PASSWORD] = user_input[CONF_PASSWORD] self.config[CONF_COUNTRY] = user_input[CONF_COUNTRY] self.config[CONF_CODE] = user_input[CONF_CODE] - self.config[CONF_CHECK_ALARM_PANEL] = user_input[CONF_CHECK_ALARM_PANEL] - self.config[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL] - self.config[CONF_DELAY_CHECK_OPERATION] = user_input[CONF_DELAY_CHECK_OPERATION] + self.config[CONF_CODE_ARM_REQUIRED] = user_input.get( + CONF_CODE_ARM_REQUIRED, DEFAULT_CODE_ARM_REQUIRED + ) + self.config[CONF_CHECK_ALARM_PANEL] = user_input.get( + CONF_CHECK_ALARM_PANEL, DEFAULT_CHECK_ALARM_PANEL + ) + self.config[CONF_SCAN_INTERVAL] = user_input.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + self.config[CONF_DELAY_CHECK_OPERATION] = user_input.get( + CONF_DELAY_CHECK_OPERATION, DEFAULT_DELAY_CHECK_OPERATION + ) self.config[CONF_DEVICE_ID] = user_input[CONF_DEVICE_ID] self.config[CONF_UNIQUE_ID] = user_input[CONF_UNIQUE_ID] self.config[CONF_DEVICE_INDIGITALL] = user_input[CONF_DEVICE_INDIGITALL] @@ -261,9 +273,9 @@ async def async_step_init( if user_input is not None: self._general_data = user_input return await self.async_step_mappings() - scan_interval = self._get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - code = "" + + code_arm_required = self._get(CONF_CODE_ARM_REQUIRED, DEFAULT_CODE_ARM_REQUIRED) delay_check_operation = self._get( CONF_DELAY_CHECK_OPERATION, DEFAULT_DELAY_CHECK_OPERATION ) @@ -274,7 +286,8 @@ async def async_step_init( schema = vol.Schema( { - vol.Optional(CONF_CODE, default=code): str, + vol.Optional(CONF_CODE, default=self._get(CONF_CODE, DEFAULT_CODE)): str, + vol.Optional(CONF_CODE_ARM_REQUIRED, default=code_arm_required): bool, vol.Optional(CONF_PERI_ALARM, default=peri_alarm): bool, vol.Optional( CONF_CHECK_ALARM_PANEL, default=check_alarm_panel diff --git a/custom_components/securitas/strings.json b/custom_components/securitas/strings.json index 9da929cb..a306bb17 100644 --- a/custom_components/securitas/strings.json +++ b/custom_components/securitas/strings.json @@ -11,6 +11,7 @@ "use_2FA": "Use 2FA", "country": "Country Code", "code": "PIN Code (leave empty for no PIN)", + "code_arm_required": "Require PIN code to arm", "PERI_alarm": "Is there a Perimetral alarm?", "check_alarm_panel": "Check alarm panel's status in every request", "scan_interval": "Update scan interval (sec)", @@ -34,6 +35,7 @@ "init": { "data": { "code": "PIN Code (leave empty for no PIN)", + "code_arm_required": "Require PIN code to arm", "PERI_alarm": "Is there a Perimetral alarm?", "check_alarm_panel": "Check alarm panel's status in every request (not recommended)", "scan_interval": "Update scan interval (sec)", @@ -51,5 +53,10 @@ } } } + }, + "exceptions": { + "invalid_pin_code": { + "message": "Invalid PIN code for {entity_id}" + } } } diff --git a/custom_components/securitas/translations/en.json b/custom_components/securitas/translations/en.json index 9da929cb..a306bb17 100644 --- a/custom_components/securitas/translations/en.json +++ b/custom_components/securitas/translations/en.json @@ -11,6 +11,7 @@ "use_2FA": "Use 2FA", "country": "Country Code", "code": "PIN Code (leave empty for no PIN)", + "code_arm_required": "Require PIN code to arm", "PERI_alarm": "Is there a Perimetral alarm?", "check_alarm_panel": "Check alarm panel's status in every request", "scan_interval": "Update scan interval (sec)", @@ -34,6 +35,7 @@ "init": { "data": { "code": "PIN Code (leave empty for no PIN)", + "code_arm_required": "Require PIN code to arm", "PERI_alarm": "Is there a Perimetral alarm?", "check_alarm_panel": "Check alarm panel's status in every request (not recommended)", "scan_interval": "Update scan interval (sec)", @@ -51,5 +53,10 @@ } } } + }, + "exceptions": { + "invalid_pin_code": { + "message": "Invalid PIN code for {entity_id}" + } } } diff --git a/custom_components/securitas/translations/es.json b/custom_components/securitas/translations/es.json index b392d621..c82eae7b 100644 --- a/custom_components/securitas/translations/es.json +++ b/custom_components/securitas/translations/es.json @@ -11,6 +11,7 @@ "use_2FA": "Usar 2FA", "country": "Código de país", "code": "Código PIN (dejar vacío si no hay PIN)", + "code_arm_required": "Requerir código PIN para armar", "PERI_alarm": "¿Hay una alarma perimetral?", "check_alarm_panel": "Comprobar el estado del panel de alarma en cada solicitud", "scan_interval": "Intervalo de actualización (seg)", @@ -34,6 +35,7 @@ "init": { "data": { "code": "Código PIN (dejar vacío si no hay PIN)", + "code_arm_required": "Requerir código PIN para armar", "PERI_alarm": "¿Hay una alarma perimetral?", "check_alarm_panel": "Comprobar el estado del panel de alarma en cada solicitud (no recomendado)", "scan_interval": "Intervalo de actualización (seg)", @@ -51,5 +53,10 @@ } } } + }, + "exceptions": { + "invalid_pin_code": { + "message": "Código PIN no válido para {entity_id}" + } } }