Add support for module PGM control#591
Conversation
Implements control of PGMs on Paradox expansion modules. (PGM4, zone extenders with PGM outputs, etc.) using the 0xA4 command class. Paradox handles "Module" PGMs differently to "Panel" PGMs which are already supported in pai.
Protocol:
- TX: A4 17 00 <module_addr> 00 00 <pgm1> <pgm2> <pgm3> <pgm4> 00*12 <cs>
- RX: A2 07 00 <module_addr> 00 00 <cs>
- Module address = Babyware enrollment slot index (1-254)
- No RAM status block; state tracked optimistically after each command
Changes:
- parsers.py: PerformModulePGMAction + PerformModulePGMActionResponse structs,
MODULE_PGM_OUTPUTS_PER_MODULE constant
- panel.py (evo): parse_message dispatch for 0xA4/0xA, control_module_pgm_outputs()
- panel.py (base): abstract control_module_pgm_outputs stub
- config.py: MODULE_PGM_ADDRESSES dict config (address -> pgm_count)
- paradox.py: _init_module_pgms(), extended control_output() for module_pgm container
- helpers.py: module_pgm topic mapping
- homeassistant.py: _publish_module_pgm_configs() for HA autodiscovery
- switch.py: ModulePGMSwitch with on_override/off_override payloads
- factory.py: make_module_pgm_switch()
- test_module_pgm_action.py: 16 tests covering build and parse against captured frames
| """ | ||
| pgm_commands = ["release"] * parsers.MODULE_PGM_OUTPUTS_PER_MODULE | ||
| pgm_commands[pgm_index - 1] = command |
There was a problem hiding this comment.
| """ | |
| pgm_commands = ["release"] * parsers.MODULE_PGM_OUTPUTS_PER_MODULE | |
| pgm_commands[pgm_index - 1] = command | |
| """ | |
| assert 1 <= pgm_index <= parsers.MODULE_PGM_OUTPUTS_PER_MODULE, "pgm_index must be between 1 and %d" % parsers.MODULE_PGM_OUTPUTS_PER_MODULE | |
| pgm_commands = ["release"] * parsers.MODULE_PGM_OUTPUTS_PER_MODULE | |
| pgm_commands[pgm_index - 1] = command |
There was a problem hiding this comment.
I'll include this change but I'm also renaming MODULE_PGM_OUTPUTS_PER_MODULE for clarity so I won't accept this suggestion exactly.
| payload_on="on_override", | ||
| payload_off="off_override", |
There was a problem hiding this comment.
Does not work with on and off? Not sure if *_override is required. Can you test?
There was a problem hiding this comment.
This works for me
> $ cat switch_fence.sh
mosquitto_pub -t "paradox/control/outputs/module4_pgm4" -m "off_override"
mosquitto_pub -t "paradox/control/outputs/module4_pgm4" -m "on_override"
switching "*_override" out for "off" and "on" - I get nothing. No relay click, no triggering of the attached device. This is for a relay on a PGM4.
| async def control_output(self, output, command) -> bool: | ||
| command = command.lower() | ||
| logger.debug(f"Control Output: {output} - {command}") | ||
|
|
||
| outputs_selected = self.storage.get_container("pgm").select(output) | ||
|
|
||
| # Not Found | ||
| if len(outputs_selected) == 0: | ||
| logger.error("No outputs selected") | ||
| return False | ||
|
|
||
| # Apply state changes | ||
| accepted = False | ||
| try: | ||
| accepted = await self.panel.control_outputs(outputs_selected, command) | ||
| except NotImplementedError: | ||
| logger.error("control_output is not implemented for this alarm type") | ||
| except asyncio.CancelledError: | ||
| logger.error("control_output canceled") | ||
| except asyncio.TimeoutError: | ||
| logger.error("control_output timeout") | ||
| # Apply state changes | ||
|
|
||
| # Refresh status | ||
| self.request_status_refresh() # Trigger status update | ||
|
|
||
| return accepted | ||
| if outputs_selected: | ||
| accepted = False | ||
| try: | ||
| accepted = await self.panel.control_outputs(outputs_selected, command) | ||
| except NotImplementedError: | ||
| logger.error("control_output is not implemented for this alarm type") | ||
| except asyncio.CancelledError: | ||
| logger.error("control_output canceled") | ||
| raise | ||
| except asyncio.TimeoutError: | ||
| logger.error("control_output timeout") | ||
| self.request_status_refresh() | ||
| return accepted | ||
|
|
||
| module_pgm_selected = self.storage.get_container("module_pgm").select(output) | ||
| if module_pgm_selected: | ||
| accepted = False | ||
| module_pgm_container = self.storage.get_container("module_pgm") | ||
| for key in module_pgm_selected: | ||
| out = module_pgm_container[key] | ||
| try: | ||
| accepted = await self.panel.control_module_pgm_outputs( | ||
| out["module_address"], out["pgm_index"], command | ||
| ) | ||
| except NotImplementedError: | ||
| logger.error("control_module_pgm_outputs is not implemented for this alarm type") | ||
| except asyncio.CancelledError: | ||
| logger.error("control_output canceled") | ||
| raise | ||
| except asyncio.TimeoutError: | ||
| logger.error("control_output timeout") | ||
| if accepted: | ||
| is_on = command in ("on", "on_override") | ||
| self.storage.update_container_object("module_pgm", out["key"], {"on": is_on}) | ||
| self.request_status_refresh() | ||
| return accepted | ||
|
|
||
| logger.error("No outputs selected") | ||
| return False |
There was a problem hiding this comment.
I think this function requires a unit test. It is quite complicated. Especially how will it distinguish if it needs to trigger PGM on the main module or external module.
There was a problem hiding this comment.
Added some tests for this.
paradox/hardware/evo/parsers.py
Outdated
| "checksum" / PacketChecksum(Bytes(1)), | ||
| ) | ||
|
|
||
| MODULE_PGM_OUTPUTS_PER_MODULE = 4 |
There was a problem hiding this comment.
I see here you have strictly 4 PGMs per external module, but in the config it is possible to specify PGM count. Which is correct? If it is always 4 then in the config should be just an array of module addresses.
There was a problem hiding this comment.
I'll rename MODULE_PGM_OUTPUTS_PER_MODULE to MODULE_PGM_PACKET_SLOTS and add a comment to make the distinction clear. The 0xA4 packet always carries exactly 4 command slots (protocol-fixed), so this is a wire-format constant. The values in MODULE_PGM_ADDRESSES are separate — they control how many PGMs PAI exposes for a given module. My PGM4 has 4 but a keypad might only have 1.
Co-authored-by: Jevgeni Kiski <yozik04@gmail.com>
Co-authored-by: Jevgeni Kiski <yozik04@gmail.com>
Co-authored-by: Jevgeni Kiski <yozik04@gmail.com>
Hi @arnaudpn this won't help with that. This provides for "Module" PGMs not the PGMs on the panel. I'm surprised that this doesn't already work, pai does provide for panel PGMs. You may need to configure the Panel PGMs for them to be available in PAI. |
|
|
Thank you @CamW! |
|
It's been a pleasure, thank you for your help getting it in @yozik04 |



Add Combus module PGM control (MODULE_PGM_ADDRESSES)
Implements control of PGMs on Paradox expansion modules (PGM4, zone
extenders with PGM outputs, etc.) using the 0xA4 command class.
Paradox handles "Module" PGMs differently to "Panel" PGMs which are
already supported in pai. State is tracked optimistically after each
command as no RAM status block is available for module PGMs.
Protocol:
Changes:
structs, MODULE_PGM_OUTPUTS_PER_MODULE constant
control_module_pgm_outputs()
module_pgm container
captured frames