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
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# securitas-direct-new-api

This repository contains the new securitas direct API that can be integrated in Home Assistant.

## Features
Expand All @@ -25,26 +26,77 @@ _or_
4. Select that entry and click the download button. ⬇️

## Setup

![Options](./docs/images/setup.png)

This integration supports config flow, so go to the list of integrations and click on add Securitas from there.

- Enter the username and password for your Securitas account.
- Use 2FA (default: yes). Uncheck this box if you want to skip the 2FA.
- 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.
- 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.

## Options

If you need to change some of the options, you can configure the integration (in HA, go to Settings -> Integrations -> Securitas Direct -> Configure)

![Options](./docs/images/options.png)

## Alarm State Mapping

Securitas Direct supports several alarm modes, but Home Assistant's alarm panel only has four buttons: **Home**, **Away**, **Night**, and **Custom Bypass**. This integration lets you choose which Securitas mode each button activates.

### Securitas Alarm Modes

| Mode | Description |
| ------------------------- | ------------------------------------- |
| Disarmed | Alarm is off |
| Partial Day | Interior sensors armed (daytime) |
| Partial Night | Interior sensors armed (nighttime) |
| Total | All interior sensors armed |
| Perimeter Only | External/outdoor sensors only |
| Partial Day + Perimeter | Daytime interior + external sensors |
| Partial Night + Perimeter | Nighttime interior + external sensors |
| Total + Perimeter | All interior + external sensors |

### How It Works

Each of the four HA alarm buttons can be mapped to any Securitas mode in the integration options (Settings -> Integrations -> Securitas Direct -> Configure). If you set a button to "Not Used", it will be hidden from the alarm panel.

When the integration checks the alarm status, it translates the Securitas response back to the correct HA state using the same mapping. For example, if you mapped the **Away** button to "Total + Perimeter", then when Securitas reports "Total + Perimeter" the alarm panel will show "Armed Away".

When switching between armed modes (e.g. from "Armed Home" to "Armed Away"), the integration automatically disarms the alarm first and then arms with the new mode. This is necessary because the Securitas API treats interior and perimeter as independent axes -- sending a new interior mode without disarming first could leave the perimeter in an unexpected state.

### Default Mappings

**Standard installations** (no perimeter sensors):

| HA Button | Securitas Mode |
| --------- | ----------------- |
| Home | Partial Day |
| Away | Total |
| Night | Partial Night |
| Custom | Not Used (hidden) |

**Perimeter installations** (external sensors enabled):

| HA Button | Securitas Mode |
| --------- | ------------------------- |
| Home | Partial Day |
| Away | Total + Perimeter |
| Night | Partial Night + Perimeter |
| Custom | Perimeter Only |

### Known Limitations

The status code for **Partial Night + Perimeter** is currently unknown. If your alarm is in this state, it will show as "Custom Bypass" in Home Assistant. If you have this situation, please [open an issue](https://github.com/guerrerotook/securitas-direct-new-api/issues) so we can identify the correct status code and add proper support.

## New Features

Added a button to update the status of your alamr using the API. Thanks to @edwin-anne.
Added a button to update the status of your alarm using the API. Thanks to @edwin-anne.

## Breaking changes

Expand Down
10 changes: 10 additions & 0 deletions custom_components/securitas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
config[CONF_MAP_AWAY] = defaults[CONF_MAP_AWAY]
config[CONF_MAP_NIGHT] = defaults[CONF_MAP_NIGHT]
config[CONF_MAP_CUSTOM] = defaults[CONF_MAP_CUSTOM]
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_MAP_HOME: config[CONF_MAP_HOME],
CONF_MAP_AWAY: config[CONF_MAP_AWAY],
CONF_MAP_NIGHT: config[CONF_MAP_NIGHT],
CONF_MAP_CUSTOM: config[CONF_MAP_CUSTOM],
},
)

Comment thread
clintongormley marked this conversation as resolved.
if CONF_DEVICE_ID in entry.data:
config[CONF_DEVICE_ID] = entry.data[CONF_DEVICE_ID]
Expand Down
19 changes: 13 additions & 6 deletions custom_components/securitas/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,20 @@ async def async_step_mappings(

# Determine defaults for mapping dropdowns
defaults = PERI_DEFAULTS if peri_alarm else STD_DEFAULTS
map_home = self._get(CONF_MAP_HOME, defaults[CONF_MAP_HOME])
map_away = self._get(CONF_MAP_AWAY, defaults[CONF_MAP_AWAY])
map_night = self._get(CONF_MAP_NIGHT, defaults[CONF_MAP_NIGHT])
map_custom = self._get(CONF_MAP_CUSTOM, defaults[CONF_MAP_CUSTOM])

# Build dropdown options based on perimeter setting
options = PERI_OPTIONS if peri_alarm else STD_OPTIONS
valid_values = {state.value for state in options}

def _valid_map(key: str) -> str:
"""Return saved mapping if valid for current options, else default."""
val = self._get(key, defaults[key])
return val if val in valid_values else defaults[key]

map_home = _valid_map(CONF_MAP_HOME)
map_away = _valid_map(CONF_MAP_AWAY)
map_night = _valid_map(CONF_MAP_NIGHT)
map_custom = _valid_map(CONF_MAP_CUSTOM)

# Build dropdown options
select_options = [
{"value": state.value, "label": STATE_LABELS[state]}
for state in options
Expand Down
52 changes: 31 additions & 21 deletions custom_components/securitas/securitas_direct_new_api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,34 @@ class SecuritasState(StrEnum):
"""Verisure alarm states - combinations of interior mode and perimeter."""
NOT_USED = "not_used"
DISARMED = "disarmed"
PARTIAL = "partial"
PARTIAL_DAY = "partial_day"
PARTIAL_NIGHT = "partial_night"
TOTAL = "total"
PERI_ONLY = "peri_only"
PARTIAL_PERI = "partial_peri"
PARTIAL_DAY_PERI = "partial_day_peri"
PARTIAL_NIGHT_PERI = "partial_night_peri"
TOTAL_PERI = "total_peri"


# Map SecuritasState -> API arm command string
STATE_TO_COMMAND: dict[SecuritasState, str] = {
SecuritasState.DISARMED: "DARM1DARMPERI",
SecuritasState.PARTIAL: "ARMDAY1",
SecuritasState.PARTIAL_DAY: "ARMDAY1",
SecuritasState.PARTIAL_NIGHT: "ARMNIGHT1",
SecuritasState.TOTAL: "ARM1",
SecuritasState.PERI_ONLY: "PERI1",
SecuritasState.PARTIAL_PERI: "ARMDAY1PERI1",
SecuritasState.PARTIAL_DAY_PERI: "ARMDAY1PERI1",
SecuritasState.PARTIAL_NIGHT_PERI: "ARMNIGHT1PERI1",
SecuritasState.TOTAL_PERI: "ARM1PERI1",
}

# Map protomResponse code -> SecuritasState
PROTO_TO_STATE: dict[str, SecuritasState] = {
"D": SecuritasState.DISARMED,
"E": SecuritasState.PERI_ONLY,
"P": SecuritasState.PARTIAL,
"B": SecuritasState.PARTIAL_PERI,
"P": SecuritasState.PARTIAL_DAY,
"Q": SecuritasState.PARTIAL_NIGHT,
"B": SecuritasState.PARTIAL_DAY_PERI,
"T": SecuritasState.TOTAL,
"A": SecuritasState.TOTAL_PERI,
}
Expand All @@ -44,43 +49,48 @@ class SecuritasState(StrEnum):
STATE_LABELS: dict[SecuritasState, str] = {
SecuritasState.NOT_USED: "Not used",
SecuritasState.DISARMED: "Disarmed",
SecuritasState.PARTIAL: "Partial",
SecuritasState.PARTIAL_DAY: "Partial Day",
SecuritasState.PARTIAL_NIGHT: "Partial Night",
SecuritasState.TOTAL: "Total",
SecuritasState.PERI_ONLY: "Perimeter only",
SecuritasState.PARTIAL_PERI: "Partial + Perimeter",
SecuritasState.PARTIAL_DAY_PERI: "Partial Day + Perimeter",
SecuritasState.PARTIAL_NIGHT_PERI: "Partial Night + Perimeter",
SecuritasState.TOTAL_PERI: "Total + Perimeter",
}

# Options available when perimeter is NOT configured
STD_OPTIONS: list[SecuritasState] = [
SecuritasState.NOT_USED,
SecuritasState.DISARMED,
SecuritasState.PARTIAL,
SecuritasState.PARTIAL_DAY,
SecuritasState.PARTIAL_NIGHT,
SecuritasState.TOTAL,
]

# Options available when perimeter IS configured
PERI_OPTIONS: list[SecuritasState] = [
SecuritasState.NOT_USED,
SecuritasState.DISARMED,
SecuritasState.PARTIAL,
SecuritasState.PARTIAL_DAY,
SecuritasState.PARTIAL_NIGHT,
SecuritasState.TOTAL,
SecuritasState.PERI_ONLY,
SecuritasState.PARTIAL_PERI,
SecuritasState.PARTIAL_DAY_PERI,
SecuritasState.PARTIAL_NIGHT_PERI,
SecuritasState.TOTAL_PERI,
]

# Default mappings matching current behavior (keyed by HA button name)
STD_DEFAULTS: dict[str, SecuritasState] = {
"map_home": SecuritasState.PARTIAL,
"map_away": SecuritasState.TOTAL,
"map_night": SecuritasState.PARTIAL,
"map_custom": SecuritasState.NOT_USED,
STD_DEFAULTS: dict[str, str] = {
"map_home": SecuritasState.PARTIAL_DAY.value,
"map_away": SecuritasState.TOTAL.value,
"map_night": SecuritasState.PARTIAL_NIGHT.value,
"map_custom": SecuritasState.NOT_USED.value,
}

PERI_DEFAULTS: dict[str, SecuritasState] = {
"map_home": SecuritasState.PARTIAL,
"map_away": SecuritasState.TOTAL_PERI,
"map_night": SecuritasState.PARTIAL_PERI,
"map_custom": SecuritasState.PERI_ONLY,
PERI_DEFAULTS: dict[str, str] = {
"map_home": SecuritasState.PARTIAL_DAY.value,
"map_away": SecuritasState.TOTAL_PERI.value,
"map_night": SecuritasState.PARTIAL_NIGHT_PERI.value,
"map_custom": SecuritasState.PERI_ONLY.value,
}
55 changes: 55 additions & 0 deletions custom_components/securitas/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured"
},
"step": {
"user": {
"data": {
"username": "Username",
"password": "Password",
"use_2FA": "Use 2FA",
"country": "Country Code",
"code": "PIN Code (leave empty for no PIN)",
"PERI_alarm": "Is there a Perimetral alarm?",
"check_alarm_panel": "Check alarm panel's status in every request",
"scan_interval": "Update scan interval (sec)",
"delay_check_operation": "Delay to check the arming and disarming operations (sec)"
},
"description": "Login into Securitas Direct.",
"title": "Login"
},
"phone_list": {
"description": "Please select the phone to send the 2FA code",
"title": "Securitas Direct 2FA"
},
"otp_challenge": {
"description": "Enter the code you received on your phone",
"title": "Securitas Direct 2FA"
}
}
},
"options": {
"step": {
"init": {
"data": {
"code": "PIN Code (leave empty for no PIN)",
"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)",
"delay_check_operation": "Delay to check the arming and disarming operation (sec)"
}
},
"mappings": {
"title": "Alarm State Mappings",
"description": "Map each Home Assistant alarm action to a Securitas alarm mode. Set to 'Not used' to disable a button.",
"data": {
"map_home": "Home",
"map_away": "Away",
"map_night": "Night",
"map_custom": "Custom"
}
}
}
}
}
10 changes: 10 additions & 0 deletions custom_components/securitas/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
"scan_interval": "Intervalo de actualización (seg)",
"delay_check_operation": "Retardo para comprobar las operaciones de armado y desarmado (seg)"
}
},
"mappings": {
"title": "Asignación de estados de alarma",
"description": "Asigna cada acción de alarma de Home Assistant a un modo de alarma Securitas. Selecciona 'No usado' para desactivar un botón.",
"data": {
"map_home": "Casa",
"map_away": "Fuera",
"map_night": "Noche",
"map_custom": "Personalizado"
}
}
}
}
Expand Down
Loading
Loading