Skip to content
Closed
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
16 changes: 12 additions & 4 deletions 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,11 +61,13 @@

DEFAULT_USE_2FA = True
DEFAULT_SCAN_INTERVAL = 120
DEFAULT_CODE_ARM_REQUIRED = True
DEFAULT_CHECK_ALARM_PANEL = True
DEFAULT_DELAY_CHECK_OPERATION = 2
DEFAULT_CODE = ""
DEFAULT_PERI_ALARM = False
DEFAULT_COUNTRY = "ES"

EMPTY_CODE = ""

PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR, Platform.LOCK]
HUB = None
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_CODE, default=DEFAULT_CODE): str,
vol.Optional(CONF_COUNTRY, default=DEFAULT_COUNTRY): str,
vol.Optional(CONF_CODE, default=EMPTY_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 All @@ -136,8 +141,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
config[CONF_PASSWORD] = entry.data[CONF_PASSWORD]
config[CONF_USE_2FA] = entry.data.get(CONF_USE_2FA, DEFAULT_USE_2FA)
config[CONF_COUNTRY] = entry.data.get(CONF_COUNTRY, None)
config[CONF_CODE] = entry.data.get(CONF_CODE, DEFAULT_CODE)
config[CONF_CODE] = entry.data.get(CONF_CODE, None)
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
64 changes: 30 additions & 34 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,
SecuritasDirectDevice,
SecuritasHub,
Expand Down Expand Up @@ -87,7 +89,6 @@ def __init__(
"""Initialize the Securitas alarm panel."""
self._state: str = AlarmControlPanelState.DISARMED
self._last_status: str = AlarmControlPanelState.DISARMED
self._changed_by: str = ""
self._device: str = installation.address
self.entity_id: str = f"securitas_direct.{installation.number}"
self._attr_unique_id: str = f"securitas_direct.{installation.number}"
Expand Down Expand Up @@ -121,6 +122,13 @@ def __init__(
self._update_unsub = async_track_time_interval(
hass, self.async_update_status, self._update_interval
)

Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

There's trailing whitespace at the end of this line that should be removed for code cleanliness.

Suggested change

Copilot uses AI. Check for mistakes.
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

Comment on lines +125 to +130
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

There's trailing whitespace at the end of this line that should be removed for code cleanliness.

Suggested change
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._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

Copilot uses AI. Check for mistakes.
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 @@ -155,21 +163,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 @@ -233,25 +226,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 that the code entered in the panel matches the code in the config only if arm require code is enable."""
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:
Expand Down Expand Up @@ -324,25 +320,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
11 changes: 9 additions & 2 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,
EMPTY_CODE,
DOMAIN,
SecuritasDirectDevice,
SecuritasHub,
Expand Down Expand Up @@ -216,6 +219,7 @@ 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_CODE_ARM_REQUIRED] = user_input.get(CONF_CODE_ARM_REQUIRED, DEFAULT_CODE_ARM_REQUIRED)
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]
Expand Down Expand Up @@ -263,18 +267,21 @@ async def async_step_init(
return await self.async_step_mappings()

scan_interval = self._get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
code = ""
delay_check_operation = self._get(
CONF_DELAY_CHECK_OPERATION, DEFAULT_DELAY_CHECK_OPERATION
)
code_arm_required = self._get(
CONF_CODE_ARM_REQUIRED, DEFAULT_CODE_ARM_REQUIRED
)
check_alarm_panel = self._get(
CONF_CHECK_ALARM_PANEL, DEFAULT_CHECK_ALARM_PANEL
)
peri_alarm = self._get(CONF_PERI_ALARM, DEFAULT_PERI_ALARM)

schema = vol.Schema(
{
vol.Optional(CONF_CODE, default=code): str,
vol.Optional(CONF_CODE, default=self._get(CONF_CODE, EMPTY_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/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?",
"check_alarm_panel": "Check alarm panel's status in every request",
"scan_interval": "Update scan interval (sec)",
Expand All @@ -29,11 +30,17 @@
}
}
},
"exceptions": {
"invalid_pin_code": {
"message": "The PIN code doesn't match the configured one"
}
},
"options": {
"step": {
"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 Down
Loading