Skip to content

Improve PIN code handling and add code_arm_required option#335

Closed
clintongormley wants to merge 5 commits into
guerrerotook:mainfrom
clintongormley:fix/pin-and-const-cleanup
Closed

Improve PIN code handling and add code_arm_required option#335
clintongormley wants to merge 5 commits into
guerrerotook:mainfrom
clintongormley:fix/pin-and-const-cleanup

Conversation

@clintongormley
Copy link
Copy Markdown
Collaborator

@clintongormley clintongormley commented Feb 25, 2026

Summary

Based on #326 by @poupounetjoyeux, rebased onto main with the arm_away_as_arm_night feature removed and several fixes applied:

  • Better PIN validation: Replace check_code() with _check_code() that raises ServiceValidationError on mismatch (shows error in UI instead of silently failing)
  • code_arm_required option: New config/options toggle to control whether a PIN is required to arm (disarm always requires PIN if configured)
  • Text PIN support: Auto-detect code_format as NUMBER or TEXT based on PIN content
  • Options flow PIN preservation: Pre-fill PIN field with existing value so submitting options without re-entering doesn't wipe it
  • Backward compatibility: Use .get() with defaults for new config keys in async_step_import to avoid KeyError on older entries
  • Translation cleanup: Fix punctuation in code_arm_required label

Changes from #326

  • Dropped arm_away_as_arm_night feature (out of scope)
  • Fixed async_step_import KeyError for older config entries missing code_arm_required
  • Fixed options flow silently clearing configured PIN
  • Fixed translation grammar ("PIN code is required to arm ?" → "Require PIN code to arm?")
  • Removed dead code

Test plan

  • Verify new install with PIN configured — code_format auto-detected correctly
  • Verify existing install without code_arm_required in config data loads without error
  • Verify options flow preserves PIN when not re-entered
  • Verify options flow allows clearing PIN by emptying the field
  • Verify code_arm_required = false allows arming without PIN
  • Verify disarm always requires PIN when configured
  • Verify wrong PIN shows error notification via ServiceValidationError

🤖 Generated with Claude Code

Closes #326

poupounetjoyeux and others added 5 commits February 25, 2026 18:31
Uses base AlarmControlPanel entity methods and attributes
Allows to disable the PIN code to arm
Correctly manage the code_format attribute if no PIN defined (for AndroidAuto)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use .get() with default for CONF_CODE_ARM_REQUIRED in async_step_import
  to avoid KeyError on older config entries
- Default PIN field to existing configured value in options flow so
  submitting without re-entering doesn't wipe the PIN
- Remove dead `code = ""` variable
- Fix translation: "PIN code is required to arm ?" → "Require PIN code to arm?"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clintongormley
Copy link
Copy Markdown
Collaborator Author

@poupounetjoyeux I've rebased your PR #326 against main and made the changes suggest by CoPilot.

Question for you: why did you add the "Require PIN code to arm?" checkbox? This is a breaking change. Before it was enough for the user to fill in the PIN to make it required (and in fact the text in the PIN field still says this).

I would remove the checkbox unless you have a good reason for keeping it?

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

poupounetjoyeux commented Feb 25, 2026

This is to allow arming without code (since its less sensitive than disarming) however in all cases disarming require the PIN
This is the HA standard of the base AlarmControlPanel and normally I set it to True by default to ensure there is no breaking change
I will rebase mine tomorrow since I'm not a big fan of letting my house security in the hands of copilot 😅

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances PIN code handling for the Securitas Direct alarm integration by introducing better validation, auto-detection of code format, and a configurable option to require PIN for arming operations.

Changes:

  • Improved PIN validation with ServiceValidationError for better user feedback
  • Added code_arm_required configuration option to control whether PIN is required for arming (disarm always requires PIN)
  • Auto-detection of code format (NUMBER vs TEXT) based on PIN content
  • Options flow now preserves existing PIN value when not re-entered
  • Backward compatibility maintained for older configuration entries

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
custom_components/securitas/translations/en.json Added translations for code_arm_required option and invalid_pin_code exception message
custom_components/securitas/config_flow.py Added code_arm_required option to config flow and options flow, with PIN preservation in options
custom_components/securitas/alarm_control_panel.py Implemented PIN validation with ServiceValidationError, auto-detection of code format, and conditional PIN requirement for arm operations
custom_components/securitas/init.py Added constants and defaults for code_arm_required, renamed DEFAULT_CODE to EMPTY_CODE for clarity

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.
Comment on lines +125 to +130

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

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.
@clintongormley
Copy link
Copy Markdown
Collaborator Author

ok, then i'll close this PR in favour of yours

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

This is to allow arming without code (since its less sensitive than disarming) however in all cases disarming require the PIN This is the HA standard of the base AlarmControlPanel and normally I set it to True by default to ensure there is no breaking change I will rebase mine tomorrow since I'm not a big fan of letting my house security in the hands of copilot 😅

@clintongormley to be clearer
The scenario is you leave the house and you forget to arm the alarm
From your car (AndroidAuto) you can arm it because no PIN code is needed to arm but to desarm it you will need your phone to enter the PIN

@clintongormley
Copy link
Copy Markdown
Collaborator Author

I'm not sure I understand - you added a checkbox that defaults to requiring a pin code in order to arm the alarm. But that doesn't work with the scenario you describe, wanting to arm the alarm from android without typing in a PIN. Also, you don't require a pin to arm the alarm today (that's why I flagged this as a breaking issue).

Ahh I think I understand what is going on. You have two problems:

  • you have a PIN that requires non-numeric characters
  • you want to arm your alarm from AndroidAuto without a PIN

The way you deal with the non-numeric characters is to type the PIN into the PIN field and have that recognised as non-numeric, so that when you need to DISARM the PIN it allows you to type in whatever you want. But that forces you to use a PIN to ARM the alarm too, so you added the checkbox.

I think that mixes concerns. A better way to do it would be to add a checkbox saying "Allow non-numeric PINs" and default it to FALSE. That way there is no-breaking change, but you still get your requirements met.

@guerrerotook
Copy link
Copy Markdown
Owner

Another thing to consider is that this pin is local to the Home Assistant installation, it's not related with securitas direct in any form. Actually you can leave your PIN blank and it will be more easy.

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

poupounetjoyeux commented Feb 25, 2026

Yes there is two different things (and so commits)

The first one is dealing with AndroidAuto that requires to have no code at all to apply actions on alarms
Since I don't want to have a breach by having no code when disarming I added the option you're asking about (I cannot disarm from the car but what I want is more to be able to arm when I forgot)
It is using the _attr_code_arm_required attribute provided by the base AlarmControlPanelEntity from HA and is normally used with the default_code option from a classical alarm panel widget (that I also handle in my rework but in our case don't really see the advantage of it)
Don't hesitate to check my reference to my HA android PR on my original PR that allows AndroidAuto to correctly deal with this scenario (will also update it tomorrow)
The other issue was with states because AndroidAuto only support disarm and arm_away and this is covered by your merged PR

The second one is having a stronger code (local to HA I know 🙂) than just digits (in my case to disarming only but in classical case for all actions)
It's also fully supported by the base AlarmControlPanelEntity using CodeFormat.TEXT and this is why I added the possibility to choose between digit only or string code (because why not for a better security ?)
The standard widget will display a digit keyboard or a textbox regarding the code format that is computed regarding the PIN you configured (is digit() or not)
This is what lot of alarm integrations provided with HA core are currently doing

Since I let the default comportment (PIN code digit or text always required when disarming and by default for arming) I don't see where you think about a breaking change. It will just add more flexibility for the user

However I will recheck when rebasing if I missed something and retest again that migrating from an existing config keep the same comportement as the actual version 😉

@clintongormley
Copy link
Copy Markdown
Collaborator Author

It is using the _attr_code_arm_required attribute provided by the base AlarmControlPanelEntity from HA and is normally used with the default_code option from a classical alarm panel widget (that I also handle in my rework but in our case don't really see the advantage of it)

Since I let the default comportment (PIN code digit or text always required when disarming and by default for arming) I don't see where you think about a breaking change. It will just add more flexibility for the user

I'm ok with the non-numeric fix, it's the _attr_code_arm_required that is breaking. You correctly say that this attribute defaults to True in AlarmControlPanelEntity from HA, but this integration has always defaulted to not using a code to arm, so adding the _attr_code_arm_required checkbox and defaulting it to True would be a breaking change for users of this integration.

In summary, I think you should add a checkbox for accepting non-numeric PINs (which would serve for both the PIN for arming and the PIN for disarming), but I wouldn't add the _attr_code_arm_required checkbox.

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

It is using the _attr_code_arm_required attribute provided by the base AlarmControlPanelEntity from HA and is normally used with the default_code option from a classical alarm panel widget (that I also handle in my rework but in our case don't really see the advantage of it)

Since I let the default comportment (PIN code digit or text always required when disarming and by default for arming) I don't see where you think about a breaking change. It will just add more flexibility for the user

I'm ok with the non-numeric fix, it's the _attr_code_arm_required that is breaking. You correctly say that this attribute defaults to True in AlarmControlPanelEntity from HA, but this integration has always defaulted to not using a code to arm, so adding the _attr_code_arm_required checkbox and defaulting it to True would be a breaking change for users of this integration.

In summary, I think you should add a checkbox for accepting non-numeric PINs (which would serve for both the PIN for arming and the PIN for disarming), but I wouldn't add the _attr_code_arm_required checkbox.

With the current code (I just checked) method self.check_code Is called for all actions and didn't currently use the HA provided attribute that is just currently ignored by this integration (wether it could have been False or True)
It means that code is required for arming and disarming and so using True as default for my new config keep the original comportment as is
Just try to test in your HA dashboard and you will see that you need to type the code to arm

There is no different PINs it's the same for disarming and (or not regarding my new flag) arming
The PIN configuration is currently string typed and so theoritically allows both type of PINs without having any breakings for already configured digits codes
Moreover with the actual config flow if you enter a PIN code with other chars than digits (which is possible) you will not be able to type it from your HA dashboard to apply actions since we returns only CodeFormat.NUMBER to the HA widget
A flag to enable or disable alphanumeric would have been needed if the config was ont typed but it's not the case

@clintongormley
Copy link
Copy Markdown
Collaborator Author

clintongormley commented Feb 26, 2026

There is no different PINs it's the same for disarming and (or not regarding my new flag) arming

This is incorrect. The PIN field in the configuration is ONLY about arming, not about disarming. I have no PIN set in that field and I can arm without typing a PIN. I do, however, have to type a PIN to disarm. Also, they can be different PINs. The disarming PIN Is passed directly to Verisure. The arming PIN is only used within HA.

See @guerrerotook's comment where he says the same thing:

#335 (comment)

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

poupounetjoyeux commented Feb 26, 2026

There is no different PINs it's the same for disarming and (or not regarding my new flag) arming

This is incorrect. The PIN field in the configuration is ONLY about arming, not about disarming. I have no PIN set in that field and I can arm without typing a PIN. I do, however, have to type a PIN to disarm. Also, they can be different PINs. The disarming PIN Is passed directly to Verisure. The arming PIN is only used within HA.

See @guerrerotook's comment where he says the same thing:

#335 (comment)

This is not what he's saying

He is saying that the PIN is not linked to the Verisure one and is only for HA dashboard purpose (and this is what I want to manage/securize)
Currently if you leave empty the PIN in your config, you can arm and disarm without typing anything
If you set one then it's required for both actions (I just tried to ensure I'm correct)

So the options is also only for HA dashboard purpospe and allows to type the code only when disarming

So regarding it's comment, my response is 'No because I want one for disarming so letting it empty doesn't fit my needs'

I just rebased the PR #326, installed it locally and tested and can confirm it matches with what I'm saying

By the same way I tested you mapping modification and don't undestand how I can map my arm_away option to do the same as an arm_night ?
I mean what 'Patial' means Partial Night or Partial Away ?

EDIT:
Just seen https://github.com/guerrerotook/securitas-direct-new-api/pull/329/changes#diff-1b9d55e1b4f4a51504358b2f2ce1a74b2f9b7f89bbeaa69937c8c9ba1a5a6445R22
In France we have this notion (probably because it's an old VFAST model) and Verisure only support Total, Partial Day and Partial Night (this is why I mapped it in my initial change) and with your new code, I tried Partial, Home and Perimeter and the alarm wont arm
Maybe we could add them back in available mapping values ?

@clintongormley
Copy link
Copy Markdown
Collaborator Author

He is saying that the PIN is not linked to the Verisure one and is only for HA dashboard purpose (and this is what I want to manage/securize) Currently if you leave empty the PIN in your config, you can arm and disarm without typing anything If you set one then it's required for both actions (I just tried to ensure I'm correct)

I looked at the code now and my assumptions were incorrect. My apologies! The PIN is never requested for arming the alarm. And the PIN is always requested for disarming the alarm. The only difference is that, if the PIN is set, HA compares the input PIN to the field value before deciding whether to pass that on to Securitas or not. I had that completely wrong.

I don't know how you managed to disarm the alarm without a PIN. In that situation what PIN would HA have passed to Securitas?

So in that case I would keep your changes, but leave the "Require PIN for arming" checkbox unticked by default, as that is the breaking change I've been concerned about.

In France we have this notion (probably because it's an old VFAST model) and Verisure only support Total, Partial Day and Partial Night (this is why I mapped it in my initial change) and with your new code, I tried Partial, Home and Perimeter and the alarm wont arm

Ah OK. In Italy and Spain, all I have is Total/Partial/None. Yes sure, I can add it back! I'll make it Total/Partial Day/Partial Night/None, where Partial Night matches to Q

@clintongormley
Copy link
Copy Markdown
Collaborator Author

@poupounetjoyeux I'm missing one status code that Verisure returns.

  • When you set the alarm to PARTIAL_DAY it returns status P
  • When you set the alarm to PARTIAL_NIGHT it returns status Q
  • When you set the alarm to PARTIAL_DAY_PERI it returns status B.

What status does it return for PARTIAL_NIGHT_PERI?

If you turn on debug logging, set the alarm to PARTIAL_NIGHT_PERI, look for the value in protomResponse

thanks

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

poupounetjoyeux commented Feb 26, 2026

No worry, I prefer someone that is challenging the code 😉

HA is passing the installation API token and not the PIN (this is what @guerrerotook said in its comment)
PIN is ONLY for HA dashboard management (see here how the token is retrieved : https://github.com/guerrerotook/securitas-direct-new-api/blob/main/custom_components/securitas/securitas_direct_new_api/apimanager.py#L306)
The access token is passed to all Verisure API calls (arming or disarming and with or without PIN)

Currently the PIN is always required for arming and disarming (and check before going further to the Verisure API) because the integration is not calling the base method _check_code but a custom one check_code (without the protected first underscore that I removed in the PR) and this method always ask for code so for me True is the default behavior

So finally, having no PIN for disarming will just hides the keyboard on your HA dashboard but works as it is currently working meaning you can arm and disarm without typing any PIN and the code arm required will be ignored (useful if you already secured HA on your phone with digital print for example)

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

@poupounetjoyeux I'm missing one status code that Verisure returns.

  • When you set the alarm to PARTIAL_DAY it returns status P
  • When you set the alarm to PARTIAL_NIGHT it returns status Q
  • When you set the alarm to PARTIAL_DAY_PERI it returns status B.

What status does it return for PARTIAL_NIGHT_PERI?

If you turn on debug logging, set the alarm to PARTIAL_NIGHT_PERI, look for the value in protomResponse

thanks

I will try to debug but typically we have no perimeter on this kind of alarm 😅

@clintongormley
Copy link
Copy Markdown
Collaborator Author

I will try to debug but typically we have no perimeter on this kind of alarm

If you can't find it, that's ok. That status just tells us what status to display on the alarm widget. If the right one is missing then it will just map to Custom. We can add a note in the docs that we need somebody to tell us what the right code is. Sooner or later somebody will run into this issue

@poupounetjoyeux
Copy link
Copy Markdown
Contributor

I will try to debug but typically we have no perimeter on this kind of alarm

If you can't find it, that's ok. That status just tells us what status to display on the alarm widget. If the right one is missing then it will just map to Custom. We can add a note in the docs that we need somebody to tell us what the right code is. Sooner or later somebody will run into this issue

Will try and update if I find it

Maybe that we could add a Verisure central version and allows mapping types regarding the version (clearly a nice to have but could be simpler for users to not make mistakes with the mapping 😁)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants