Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 9 additions & 1 deletion custom_components/securitas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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]
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
)
Expand Down
61 changes: 28 additions & 33 deletions custom_components/securitas/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
},
Comment on lines +243 to +247
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServiceValidationError references translation_key="invalid_pin_code", but there is no corresponding translation entry in the integration’s translation files. Add an exceptions.invalid_pin_code translation (and ensure placeholders like entity_id are defined/used) so Home Assistant can render a proper error message.

Suggested change
translation_domain=DOMAIN,
translation_key="invalid_pin_code",
translation_placeholders={
"entity_id": self.entity_id,
},
message=f"Invalid PIN code for alarm entity {self.entity_id}",

Copilot uses AI. Check for mistakes.
)
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:
Expand Down Expand Up @@ -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)

Expand Down
25 changes: 19 additions & 6 deletions custom_components/securitas/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
)
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions custom_components/securitas/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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)",
Expand All @@ -51,5 +53,10 @@
}
}
}
},
"exceptions": {
"invalid_pin_code": {
"message": "Invalid PIN code for {entity_id}"
}
}
}
7 changes: 7 additions & 0 deletions custom_components/securitas/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Comment thread
clintongormley marked this conversation as resolved.
"check_alarm_panel": "Check alarm panel's status in every request",
"scan_interval": "Update scan interval (sec)",
Expand All @@ -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)",
Expand All @@ -51,5 +53,10 @@
}
}
}
},
"exceptions": {
"invalid_pin_code": {
"message": "Invalid PIN code for {entity_id}"
}
}
}
7 changes: 7 additions & 0 deletions custom_components/securitas/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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)",
Expand All @@ -51,5 +53,10 @@
}
}
}
},
"exceptions": {
"invalid_pin_code": {
"message": "Código PIN no válido para {entity_id}"
}
}
}
Loading