conson-xp 2.0.1__tar.gz → 2.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. {conson_xp-2.0.1 → conson_xp-2.0.3}/PKG-INFO +1 -1
  2. {conson_xp-2.0.1 → conson_xp-2.0.3}/pyproject.toml +1 -1
  3. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/__init__.py +1 -1
  4. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/homekit/homekit_config.py +8 -0
  5. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/actiontable_serializer.py +4 -4
  6. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/actiontable/actiontable_upload_service.py +8 -1
  7. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/term/homekit_accessory_driver.py +67 -11
  8. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/term/homekit_service.py +168 -12
  9. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/homekit.py +23 -0
  10. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_actiontable_serializer.py +111 -1
  11. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_actiontable_upload_service.py +6 -2
  12. {conson_xp-2.0.1 → conson_xp-2.0.3}/LICENSE +0 -0
  13. {conson_xp-2.0.1 → conson_xp-2.0.3}/README.md +0 -0
  14. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/__init__.py +0 -0
  15. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/__main__.py +0 -0
  16. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/__init__.py +0 -0
  17. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/__init__.py +0 -0
  18. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus.py +0 -0
  19. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_actiontable_commands.py +0 -0
  20. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_autoreport_commands.py +0 -0
  21. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_blink_commands.py +0 -0
  22. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_config_commands.py +0 -0
  23. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_custom_commands.py +0 -0
  24. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_datapoint_commands.py +0 -0
  25. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_discover_commands.py +0 -0
  26. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_event_commands.py +0 -0
  27. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_export_commands.py +0 -0
  28. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_lightlevel_commands.py +0 -0
  29. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_linknumber_commands.py +0 -0
  30. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_modulenumber_commands.py +0 -0
  31. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_msactiontable_commands.py +0 -0
  32. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_output_commands.py +0 -0
  33. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_raw_commands.py +0 -0
  34. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_receive_commands.py +0 -0
  35. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/conbus/conbus_scan_commands.py +0 -0
  36. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/file_commands.py +0 -0
  37. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/module_commands.py +0 -0
  38. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/reverse_proxy_commands.py +0 -0
  39. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/server/__init__.py +0 -0
  40. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/server/server_commands.py +0 -0
  41. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/__init__.py +0 -0
  42. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/telegram.py +0 -0
  43. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/telegram_blink_commands.py +0 -0
  44. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/telegram_checksum_commands.py +0 -0
  45. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/telegram_discover_commands.py +0 -0
  46. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/telegram_linknumber_commands.py +0 -0
  47. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/telegram_parse_commands.py +0 -0
  48. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/telegram/telegram_version_commands.py +0 -0
  49. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/term/__init__.py +0 -0
  50. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/term/term.py +0 -0
  51. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/commands/term/term_commands.py +0 -0
  52. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/main.py +0 -0
  53. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/__init__.py +0 -0
  54. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/click_tree.py +0 -0
  55. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/datapoint_type_choice.py +0 -0
  56. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/decorators.py +0 -0
  57. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/error_handlers.py +0 -0
  58. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/formatters.py +0 -0
  59. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/module_type_choice.py +0 -0
  60. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/serial_number_type.py +0 -0
  61. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/cli/utils/system_function_choice.py +0 -0
  62. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/__init__.py +0 -0
  63. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/actiontable/__init__.py +0 -0
  64. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/actiontable/actiontable.py +0 -0
  65. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/actiontable/actiontable_type.py +0 -0
  66. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/actiontable/msactiontable_xp20.py +0 -0
  67. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/actiontable/msactiontable_xp24.py +0 -0
  68. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/actiontable/msactiontable_xp33.py +0 -0
  69. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/__init__.py +0 -0
  70. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus.py +0 -0
  71. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_autoreport.py +0 -0
  72. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_blink.py +0 -0
  73. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_client_config.py +0 -0
  74. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_connection_status.py +0 -0
  75. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_custom.py +0 -0
  76. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_datapoint.py +0 -0
  77. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_discover.py +0 -0
  78. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_event_list.py +0 -0
  79. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_event_raw.py +0 -0
  80. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_export.py +0 -0
  81. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_lightlevel.py +0 -0
  82. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_linknumber.py +0 -0
  83. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_logger_config.py +0 -0
  84. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_output.py +0 -0
  85. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_raw.py +0 -0
  86. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_receive.py +0 -0
  87. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/conbus/conbus_writeconfig.py +0 -0
  88. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/config/__init__.py +0 -0
  89. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/config/conson_module_config.py +0 -0
  90. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/homekit/__init__.py +0 -0
  91. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/homekit/homekit_accessory.py +0 -0
  92. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/log_entry.py +0 -0
  93. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/protocol/__init__.py +0 -0
  94. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/protocol/conbus_protocol.py +0 -0
  95. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/response.py +0 -0
  96. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/__init__.py +0 -0
  97. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/action_type.py +0 -0
  98. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/datapoint_type.py +0 -0
  99. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/event_telegram.py +0 -0
  100. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/event_type.py +0 -0
  101. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/input_action_type.py +0 -0
  102. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/input_type.py +0 -0
  103. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/module_type.py +0 -0
  104. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/module_type_code.py +0 -0
  105. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/output_telegram.py +0 -0
  106. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/reply_telegram.py +0 -0
  107. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/system_function.py +0 -0
  108. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/system_telegram.py +0 -0
  109. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/telegram.py +0 -0
  110. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/telegram_type.py +0 -0
  111. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/telegram/timeparam_type.py +0 -0
  112. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/term/__init__.py +0 -0
  113. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/term/accessory_state.py +0 -0
  114. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/term/connection_state.py +0 -0
  115. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/term/module_state.py +0 -0
  116. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/term/protocol_keys_config.py +0 -0
  117. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/term/status_message.py +0 -0
  118. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/term/telegram_display.py +0 -0
  119. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/models/write_config_type.py +0 -0
  120. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/__init__.py +0 -0
  121. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/__init__.py +0 -0
  122. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/download_state_machine.py +0 -0
  123. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/msactiontable_serializer.py +0 -0
  124. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/msactiontable_xp20_serializer.py +0 -0
  125. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/msactiontable_xp24_serializer.py +0 -0
  126. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/msactiontable_xp33_serializer.py +0 -0
  127. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/actiontable/serializer_protocol.py +0 -0
  128. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/__init__.py +0 -0
  129. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/actiontable/__init__.py +0 -0
  130. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/actiontable/actiontable_download_service.py +0 -0
  131. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/actiontable/actiontable_list_service.py +0 -0
  132. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/actiontable/actiontable_show_service.py +0 -0
  133. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_blink_all_service.py +0 -0
  134. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_blink_service.py +0 -0
  135. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_custom_service.py +0 -0
  136. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_datapoint_queryall_service.py +0 -0
  137. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_datapoint_service.py +0 -0
  138. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_discover_service.py +0 -0
  139. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_event_list_service.py +0 -0
  140. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_event_raw_service.py +0 -0
  141. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_export_actiontable_service.py +0 -0
  142. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_export_service.py +0 -0
  143. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_output_service.py +0 -0
  144. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_raw_service.py +0 -0
  145. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_receive_service.py +0 -0
  146. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/conbus_scan_service.py +0 -0
  147. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/conbus/write_config_service.py +0 -0
  148. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/log_file_service.py +0 -0
  149. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/module_type_service.py +0 -0
  150. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/protocol/__init__.py +0 -0
  151. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/protocol/conbus_event_protocol.py +0 -0
  152. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/reverse_proxy_service.py +0 -0
  153. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/__init__.py +0 -0
  154. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/base_server_service.py +0 -0
  155. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/client_buffer_manager.py +0 -0
  156. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/cp20_server_service.py +0 -0
  157. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/device_service_factory.py +0 -0
  158. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/server_service.py +0 -0
  159. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/xp130_server_service.py +0 -0
  160. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/xp20_server_service.py +0 -0
  161. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/xp230_server_service.py +0 -0
  162. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/xp24_server_service.py +0 -0
  163. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/server/xp33_server_service.py +0 -0
  164. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/__init__.py +0 -0
  165. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_blink_service.py +0 -0
  166. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_checksum_service.py +0 -0
  167. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_datapoint_service.py +0 -0
  168. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_discover_service.py +0 -0
  169. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_link_number_service.py +0 -0
  170. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_output_service.py +0 -0
  171. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_service.py +0 -0
  172. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/telegram/telegram_version_service.py +0 -0
  173. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/term/__init__.py +0 -0
  174. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/term/protocol_monitor_service.py +0 -0
  175. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/services/term/state_monitor_service.py +0 -0
  176. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/__init__.py +0 -0
  177. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/homekit.tcss +0 -0
  178. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/protocol.py +0 -0
  179. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/protocol.tcss +0 -0
  180. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/state.py +0 -0
  181. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/state.tcss +0 -0
  182. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/widgets/__init__.py +0 -0
  183. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/widgets/help_menu.py +0 -0
  184. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/widgets/modules_list.py +0 -0
  185. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/widgets/protocol_log.py +0 -0
  186. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/widgets/room_list.py +0 -0
  187. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/term/widgets/status_footer.py +0 -0
  188. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/__init__.py +0 -0
  189. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/checksum.py +0 -0
  190. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/dependencies.py +0 -0
  191. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/event_helper.py +0 -0
  192. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/logging.py +0 -0
  193. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/serialization.py +0 -0
  194. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/state_machine.py +0 -0
  195. {conson_xp-2.0.1 → conson_xp-2.0.3}/src/xp/utils/time_utils.py +0 -0
  196. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/.coverage +0 -0
  197. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/__init__.py +0 -0
  198. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/conftest.py +0 -0
  199. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/.coverage +0 -0
  200. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/__init__.py +0 -0
  201. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/telegram_test_data.py +0 -0
  202. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_actiontable_integration.py +0 -0
  203. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_api/.coverage +0 -0
  204. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_api/__init__.py +0 -0
  205. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_blink_integration.py +0 -0
  206. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_checksum_integration.py +0 -0
  207. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_conbus_blink_integration.py +0 -0
  208. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_conbus_datapoint_integration.py +0 -0
  209. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_conbus_raw_integration.py +0 -0
  210. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_conbus_receive_integration.py +0 -0
  211. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_discovery_integration.py +0 -0
  212. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_event_telegram_integration.py +0 -0
  213. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_link_number_integration.py +0 -0
  214. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_module_integration.py +0 -0
  215. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_output_integration.py +0 -0
  216. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_reverse_proxy_integration.py +0 -0
  217. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_system_reply_telegram_integration.py +0 -0
  218. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_term_logging_integration.py +0 -0
  219. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_version_integration.py +0 -0
  220. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_xp20_action_table_integration.py +0 -0
  221. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/integration/test_xp24_action_table_integration.py +0 -0
  222. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/__init__.py +0 -0
  223. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_api/__init__.py +0 -0
  224. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/__init__.py +0 -0
  225. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_click_tree.py +0 -0
  226. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_conbus_actiontable_commands.py +0 -0
  227. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_conbus_blink_commands.py +0 -0
  228. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_conbus_msactiontable_upload_commands.py +0 -0
  229. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_datapoint_type_choice.py +0 -0
  230. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_decorators.py +0 -0
  231. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_error_handlers.py +0 -0
  232. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_formatters.py +0 -0
  233. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_serial_number_type.py +0 -0
  234. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_system_function_choice.py +0 -0
  235. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_cli/test_term_commands.py +0 -0
  236. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_encoding/__init__.py +0 -0
  237. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_encoding/test_latin1_edge_cases.py +0 -0
  238. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/__init__.py +0 -0
  239. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_conbus.py +0 -0
  240. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_conbus_client_send.py +0 -0
  241. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_conbus_discover.py +0 -0
  242. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_conbus_linknumber.py +0 -0
  243. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_event_telegram.py +0 -0
  244. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_log_entry.py +0 -0
  245. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_logger_config.py +0 -0
  246. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_module_type.py +0 -0
  247. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_reply_telegram.py +0 -0
  248. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_system_telegram.py +0 -0
  249. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_system_telegram_enhancements.py +0 -0
  250. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_term/__init__.py +0 -0
  251. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_term/test_accessory_state.py +0 -0
  252. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_version_telegram.py +0 -0
  253. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_write_config_type.py +0 -0
  254. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_xp20_action_table.py +0 -0
  255. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_xp24_action_table.py +0 -0
  256. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_xp24_action_table_short_format.py +0 -0
  257. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_models/test_xp24_action_telegram.py +0 -0
  258. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/__init__.py +0 -0
  259. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_actiontable_download_service.py +0 -0
  260. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_actiontable_service.py +0 -0
  261. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_base_server_service.py +0 -0
  262. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_blink_service.py +0 -0
  263. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_checksum_service.py +0 -0
  264. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_client_buffer_manager.py +0 -0
  265. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_blink_service.py +0 -0
  266. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_event_list_service.py +0 -0
  267. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_event_protocol.py +0 -0
  268. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_event_raw_service.py +0 -0
  269. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_output_service.py +0 -0
  270. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_raw_service.py +0 -0
  271. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_receive_service.py +0 -0
  272. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_reverse_proxy_service.py +0 -0
  273. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_conbus_scan_service.py +0 -0
  274. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_device_service_factory.py +0 -0
  275. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_discovery_service.py +0 -0
  276. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_homekit_service.py +0 -0
  277. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_log_file_service.py +0 -0
  278. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_module_type_service.py +0 -0
  279. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_protocol_monitor_service.py +0 -0
  280. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_server_service.py +0 -0
  281. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_state_monitor_service.py +0 -0
  282. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_telegram_input_service.py +0 -0
  283. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_telegram_output_service.py +0 -0
  284. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_telegram_service.py +0 -0
  285. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_version_service.py +0 -0
  286. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_xp20_action_table_serializer.py +0 -0
  287. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_xp24_action_service.py +0 -0
  288. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_xp24_action_table_serializer.py +0 -0
  289. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_xp24_action_table_service.py +0 -0
  290. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_xp33_action_table_serializer.py +0 -0
  291. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_xp33_short_format.py +0 -0
  292. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_services/test_xp_server_services.py +0 -0
  293. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_tui/__init__.py +0 -0
  294. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_tui/test_homekit_app.py +0 -0
  295. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_tui/test_protocol_log.py +0 -0
  296. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_tui/test_room_list.py +0 -0
  297. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_utils/__init__.py +0 -0
  298. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_utils/test_checksum.py +0 -0
  299. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_utils/test_event_helper.py +0 -0
  300. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_utils/test_logging.py +0 -0
  301. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_utils/test_serialization.py +0 -0
  302. {conson_xp-2.0.1 → conson_xp-2.0.3}/tests/unit/test_utils/test_time_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -22,7 +22,7 @@ dependencies = [
22
22
  ]
23
23
  requires-python = ">=3.11"
24
24
  readme = "README.md"
25
- version = "2.0.1"
25
+ version = "2.0.3"
26
26
 
27
27
  [project.license]
28
28
  file = "LICENSE"
@@ -4,7 +4,7 @@ XP CLI tool for remote console bus operations.
4
4
  conson-xp package.
5
5
  """
6
6
 
7
- __version__ = "2.0.1"
7
+ __version__ = "2.0.3"
8
8
  __manufacturer__ = "salchichon"
9
9
  __model__ = "xp.cli"
10
10
  __serial__ = "2025.09.23.000"
@@ -66,6 +66,10 @@ class HomekitAccessoryConfig(BaseModel):
66
66
  on_action: on code for the accessory.
67
67
  off_action: off code for the accessory.
68
68
  toggle_action: Optional toggle action code for the accessory.
69
+ dimup_action: Optional dim up action code for the dimmable accessory.
70
+ dimdown_action: Optional dim down action code for the dimmable accessory.
71
+ levelup_action: Optional level up action code for the dimmable accessory.
72
+ leveldown_action: Optional level down action code for the dimmable accessory.
69
73
  hap_accessory: Optional HAP accessory identifier.
70
74
  """
71
75
 
@@ -78,6 +82,10 @@ class HomekitAccessoryConfig(BaseModel):
78
82
  on_action: str
79
83
  off_action: str
80
84
  toggle_action: Optional[str] = None
85
+ dimup_action: Optional[str] = None
86
+ dimdown_action: Optional[str] = None
87
+ levelup_action: Optional[str] = None
88
+ leveldown_action: Optional[str] = None
81
89
  hap_accessory: Optional[int] = None
82
90
 
83
91
 
@@ -65,8 +65,8 @@ class ActionTableSerializer(ActionTableSerializerProtocol):
65
65
  link_number = de_bcd(data[i + 1])
66
66
  module_input = de_bcd(data[i + 2])
67
67
 
68
- # Extract output and command from byte 3
69
- module_output = lower3(data[i + 3])
68
+ # Extract output (0-indexed in wire format, convert to 1-indexed) and command
69
+ module_output = lower3(data[i + 3]) + 1
70
70
  command_raw = upper5(data[i + 3])
71
71
 
72
72
  parameter_raw = byte_to_unsigned(data[i + 4])
@@ -125,8 +125,8 @@ class ActionTableSerializer(ActionTableSerializerProtocol):
125
125
  link_byte = to_bcd(entry.link_number)
126
126
  input_byte = to_bcd(entry.module_input)
127
127
 
128
- # Combine output (lower 3 bits) and command (upper 5 bits)
129
- output_command_byte = (entry.module_output & 0x07) | (
128
+ # Combine output (lower 3 bits, 0-indexed) and command (upper 5 bits)
129
+ output_command_byte = ((entry.module_output - 1) & 0x07) | (
130
130
  (entry.command.value & 0x1F) << 3
131
131
  )
132
132
 
@@ -81,6 +81,7 @@ class ActionTableUploadService:
81
81
  # Upload state
82
82
  self.upload_data_chunks: list[str] = []
83
83
  self.current_chunk_index: int = 0
84
+ self._eof_sent: bool = False
84
85
 
85
86
  # Set up logging
86
87
  self.logger = logging.getLogger(__name__)
@@ -173,7 +174,7 @@ class ActionTableUploadService:
173
174
  )
174
175
  self.current_chunk_index += 1
175
176
  self.on_progress.emit(".")
176
- else:
177
+ elif not self._eof_sent:
177
178
  # All chunks sent, send EOF
178
179
  self.logger.debug("All chunks sent, sending EOF")
179
180
  self.conbus_protocol.send_telegram(
@@ -182,7 +183,13 @@ class ActionTableUploadService:
182
183
  system_function=SystemFunction.EOF,
183
184
  data_value="00",
184
185
  )
186
+ self.on_progress.emit("END")
187
+ self.logger.debug("EOF sent, waiting for last ACK")
188
+ self._eof_sent = True
189
+ else:
190
+ self.logger.debug("Last ACK received, closing connection")
185
191
  self.on_finish.emit(True)
192
+
186
193
  elif reply_telegram.system_function == SystemFunction.NAK:
187
194
  self.logger.debug("Received NAK during upload")
188
195
  self.failed("Upload failed: NAK received")
@@ -10,9 +10,18 @@ from pyhap.const import CATEGORY_LIGHTBULB, CATEGORY_OUTLET
10
10
 
11
11
  from xp.models.homekit.homekit_config import HomekitConfig
12
12
 
13
+ # Callback type: (accessory_name, is_on, brightness_or_none)
14
+ OnSetCallback = Callable[[str, bool, Optional[int]], None]
15
+
13
16
 
14
17
  class XPAccessory(Accessory):
15
- """Single accessory wrapping a Conbus output."""
18
+ """
19
+ Single accessory wrapping a Conbus output.
20
+
21
+ Attributes:
22
+ logger: Logger instance for this accessory.
23
+ current_brightness: Current brightness value 0-100.
24
+ """
16
25
 
17
26
  def __init__(
18
27
  self,
@@ -35,12 +44,19 @@ class XPAccessory(Accessory):
35
44
  super().__init__(driver._driver, display_name, aid=aid)
36
45
  self._hk_driver = driver
37
46
  self._accessory_id = name
47
+ self._is_dimmable = service_type == "dimminglight"
48
+ self._char_brightness: Optional[object] = None
49
+ self._current_brightness: int = 100
38
50
  self.logger = logging.getLogger(__name__)
39
51
 
40
- if service_type == "dimminglight":
52
+ if self._is_dimmable:
41
53
  self.category = CATEGORY_LIGHTBULB
42
54
  serv = self.add_preload_service("Lightbulb", chars=["On", "Brightness"])
43
- # Note: Brightness setter_callback deferred to future update
55
+ self._char_brightness = serv.configure_char(
56
+ "Brightness",
57
+ setter_callback=self._set_brightness,
58
+ value=self._current_brightness,
59
+ )
44
60
  elif service_type == "outlet":
45
61
  self.category = CATEGORY_OUTLET
46
62
  serv = self.add_preload_service("Outlet")
@@ -58,16 +74,36 @@ class XPAccessory(Accessory):
58
74
  value: True for on, False for off.
59
75
  """
60
76
  if self._hk_driver._on_set:
61
- self._hk_driver._on_set(self._accessory_id, value)
77
+ self._hk_driver._on_set(self._accessory_id, value, None)
78
+
79
+ def _set_brightness(self, value: int) -> None:
80
+ """
81
+ Handle HomeKit set brightness request.
82
+
83
+ Args:
84
+ value: Brightness value 0-100.
85
+ """
86
+ if self._hk_driver._on_set:
87
+ self._hk_driver._on_set(self._accessory_id, True, value)
88
+ self._current_brightness = value
62
89
 
63
- def update_state(self, is_on: bool) -> None:
90
+ def update_state(self, is_on: bool, brightness: Optional[int] = None) -> None:
64
91
  """
65
92
  Update accessory state from Conbus event.
66
93
 
67
94
  Args:
68
95
  is_on: True if accessory is on, False otherwise.
96
+ brightness: Optional brightness value 0-100.
69
97
  """
70
98
  self._char_on.set_value(is_on)
99
+ if brightness is not None and self._char_brightness:
100
+ self._char_brightness.set_value(brightness) # type: ignore[attr-defined]
101
+ self._current_brightness = brightness
102
+
103
+ @property
104
+ def current_brightness(self) -> int:
105
+ """Get current brightness value."""
106
+ return self._current_brightness
71
107
 
72
108
 
73
109
  class HomekitAccessoryDriver:
@@ -84,14 +120,15 @@ class HomekitAccessoryDriver:
84
120
  self._homekit_config = homekit_config
85
121
  self._driver: Optional[AccessoryDriver] = None
86
122
  self._accessories: Dict[str, XPAccessory] = {}
87
- self._on_set: Optional[Callable[[str, bool], None]] = None
123
+ self._on_set: Optional[OnSetCallback] = None
88
124
 
89
- def set_callback(self, on_set: Callable[[str, bool], None]) -> None:
125
+ def set_callback(self, on_set: OnSetCallback) -> None:
90
126
  """
91
127
  Set callback for HomeKit set events.
92
128
 
93
129
  Args:
94
- on_set: Callback(accessory_name, is_on) called when HomeKit app toggles.
130
+ on_set: Callback(accessory_name, is_on, brightness) called when HomeKit app changes state.
131
+ brightness is None for on/off only, or 0-100 for dimming.
95
132
  """
96
133
  self._on_set = on_set
97
134
 
@@ -157,15 +194,34 @@ class HomekitAccessoryDriver:
157
194
  except Exception as e:
158
195
  self.logger.error(f"Error stopping AccessoryDriver: {e}", exc_info=True)
159
196
 
160
- def update_state(self, accessory_name: str, is_on: bool) -> None:
197
+ def update_state(
198
+ self, accessory_name: str, is_on: bool, brightness: Optional[int] = None
199
+ ) -> None:
161
200
  """
162
201
  Update accessory state from Conbus event.
163
202
 
164
203
  Args:
165
204
  accessory_name: Accessory name to update.
166
205
  is_on: True if accessory is on, False otherwise.
206
+ brightness: Optional brightness value 0-100.
167
207
  """
168
- if acc := self._accessories.get(accessory_name):
169
- acc.update_state(is_on)
208
+ acc = self._accessories.get(accessory_name)
209
+ if acc:
210
+ acc.update_state(is_on, brightness)
170
211
  else:
171
212
  self.logger.warning(f"Unknown accessory name: {accessory_name}")
213
+
214
+ def get_brightness(self, accessory_name: str) -> int:
215
+ """
216
+ Get current brightness for an accessory.
217
+
218
+ Args:
219
+ accessory_name: Accessory name.
220
+
221
+ Returns:
222
+ Current brightness 0-100, defaults to 100 if not found.
223
+ """
224
+ acc = self._accessories.get(accessory_name)
225
+ if acc:
226
+ return acc.current_brightness
227
+ return 100
@@ -81,6 +81,9 @@ class HomekitService:
81
81
  # Set up HomeKit callback
82
82
  self._accessory_driver.set_callback(self._on_homekit_set)
83
83
 
84
+ # Track active level action: (accessory_id, action_type) or None
85
+ self._active_level_action: Optional[tuple[str, str]] = None
86
+
84
87
  # Connect to protocol signals
85
88
  self._connect_signals()
86
89
 
@@ -297,23 +300,82 @@ class HomekitService:
297
300
  await self._accessory_driver.stop()
298
301
  self.cleanup()
299
302
 
300
- def _on_homekit_set(self, accessory_name: str, is_on: bool) -> None:
303
+ def _on_homekit_set(
304
+ self, accessory_name: str, is_on: bool, brightness: Optional[int]
305
+ ) -> None:
301
306
  """
302
- Handle HomeKit app toggle request.
307
+ Handle HomeKit app set request (on/off or brightness).
303
308
 
304
309
  Args:
305
310
  accessory_name: Accessory name from HomeKit.
306
311
  is_on: True for on, False for off.
312
+ brightness: Brightness value 0-100, or None for on/off only.
307
313
  """
308
314
  config = self._find_accessory_config(accessory_name)
309
- if config:
315
+ if not config:
316
+ self.logger.warning(f"No config found for accessory: {accessory_name}")
317
+ return
318
+
319
+ if brightness is not None:
320
+ # Handle brightness change
321
+ self._handle_brightness_change(accessory_name, config, brightness)
322
+ else:
323
+ # Handle on/off toggle
310
324
  action = config.on_action if is_on else config.off_action
311
325
  self.send_action(action)
312
326
  self.on_status_message.emit(
313
327
  f"HomeKit: {accessory_name} {'ON' if is_on else 'OFF'}"
314
328
  )
329
+
330
+ def _handle_brightness_change(
331
+ self,
332
+ accessory_name: str,
333
+ config: "HomekitAccessoryConfig",
334
+ target_brightness: int,
335
+ ) -> None:
336
+ """
337
+ Handle brightness change by sending dimup/dimdown actions.
338
+
339
+ Calculates delta from current brightness and sends appropriate
340
+ number of LEVELINC or LEVELDEC commands (step = 10%).
341
+
342
+ Args:
343
+ accessory_name: Accessory name.
344
+ config: Accessory configuration.
345
+ target_brightness: Target brightness 0-100.
346
+ """
347
+ current = self._accessory_driver.get_brightness(accessory_name)
348
+ delta = target_brightness - current
349
+
350
+ if delta == 0:
351
+ return
352
+
353
+ # Determine action and steps (10% per step)
354
+ step_size = 10
355
+ steps = abs(delta) // step_size
356
+
357
+ if delta > 0:
358
+ # Increase brightness
359
+ if not config.dimup_action:
360
+ self.logger.warning(f"No dimup_action for {accessory_name}")
361
+ return
362
+ action = config.dimup_action
363
+ direction = "+"
315
364
  else:
316
- self.logger.warning(f"No config found for accessory: {accessory_name}")
365
+ # Decrease brightness
366
+ if not config.dimdown_action:
367
+ self.logger.warning(f"No dimdown_action for {accessory_name}")
368
+ return
369
+ action = config.dimdown_action
370
+ direction = "-"
371
+
372
+ # Send action for each step
373
+ for _ in range(steps):
374
+ self.send_action(action)
375
+
376
+ self.on_status_message.emit(
377
+ f"HomeKit: {accessory_name} {current}% → {target_brightness}% ({direction}{steps * step_size}%)"
378
+ )
317
379
 
318
380
  def send_action(self, action: str) -> None:
319
381
  """
@@ -421,12 +483,15 @@ class HomekitService:
421
483
  Returns:
422
484
  True if command was sent, False otherwise.
423
485
  """
486
+ config = self._find_accessory_config_by_id(accessory_id)
424
487
  state = self._accessory_states.get(accessory_id)
425
- if not state:
488
+ if not config or not state or not config.dimup_action:
489
+ self.logger.warning(f"No config for accessory {accessory_id}")
426
490
  return False
427
- # TODO: Implement dimmer control
428
- self.on_status_message.emit(f"Dimmer+ {state.accessory_name} (not implemented)")
429
- return False
491
+
492
+ self.send_action(config.dimup_action)
493
+ self.on_status_message.emit(f"Dim+ {state.accessory_name}")
494
+ return True
430
495
 
431
496
  def decrease_dimmer(self, accessory_id: str) -> bool:
432
497
  """
@@ -438,12 +503,103 @@ class HomekitService:
438
503
  Returns:
439
504
  True if command was sent, False otherwise.
440
505
  """
506
+ config = self._find_accessory_config_by_id(accessory_id)
441
507
  state = self._accessory_states.get(accessory_id)
442
- if not state:
508
+ if not config or not state or not config.dimdown_action:
509
+ self.logger.warning(f"No config for accessory {accessory_id}")
443
510
  return False
444
- # TODO: Implement dimmer control
445
- self.on_status_message.emit(f"Dimmer- {state.accessory_name} (not implemented)")
446
- return False
511
+
512
+ self.send_action(config.dimdown_action)
513
+ self.on_status_message.emit(f"Dim- {state.accessory_name}")
514
+ return True
515
+
516
+ def levelup_selected(self, accessory_id: str) -> bool:
517
+ """
518
+ Increase level for accessory (toggle Make/Break).
519
+
520
+ First press sends Make (M), second press sends Break (B).
521
+
522
+ Args:
523
+ accessory_id: Accessory ID (e.g., "A12_1").
524
+
525
+ Returns:
526
+ True if command was sent, False otherwise.
527
+ """
528
+ config = self._find_accessory_config_by_id(accessory_id)
529
+ state = self._accessory_states.get(accessory_id)
530
+ if not config or not state or not config.levelup_action:
531
+ self.logger.warning(f"No config for accessory {accessory_id}")
532
+ return False
533
+
534
+ return self._send_level_action(
535
+ accessory_id, "levelup", config.levelup_action, state.accessory_name
536
+ )
537
+
538
+ def leveldown_selected(self, accessory_id: str) -> bool:
539
+ """
540
+ Decrease level for accessory (toggle Make/Break).
541
+
542
+ First press sends Make (M), second press sends Break (B).
543
+
544
+ Args:
545
+ accessory_id: Accessory ID (e.g., "A12_1").
546
+
547
+ Returns:
548
+ True if command was sent, False otherwise.
549
+ """
550
+ config = self._find_accessory_config_by_id(accessory_id)
551
+ state = self._accessory_states.get(accessory_id)
552
+ if not config or not state or not config.leveldown_action:
553
+ self.logger.warning(f"No config for accessory {accessory_id}")
554
+ return False
555
+
556
+ return self._send_level_action(
557
+ accessory_id, "leveldown", config.leveldown_action, state.accessory_name
558
+ )
559
+
560
+ def _send_level_action(
561
+ self, accessory_id: str, action_type: str, action: str, name: str
562
+ ) -> bool:
563
+ """
564
+ Send level action with Make/Break toggle.
565
+
566
+ Args:
567
+ accessory_id: Accessory ID.
568
+ action_type: "levelup" or "leveldown".
569
+ action: Action code (e.g., "E02L13I15").
570
+ name: Accessory name for status message.
571
+
572
+ Returns:
573
+ True if command was sent.
574
+ """
575
+ current = self._active_level_action
576
+
577
+ # If same action is active, send Break and clear
578
+ if current and current[0] == accessory_id and current[1] == action_type:
579
+ self._conbus_protocol.send_raw_telegram(f"{action}B")
580
+ self._active_level_action = None
581
+ direction = "+" if action_type == "levelup" else "-"
582
+ self.on_status_message.emit(f"Level{direction} {name} [B]")
583
+ return True
584
+
585
+ # If different action is active, send Break for it first
586
+ if current:
587
+ old_config = self._find_accessory_config_by_id(current[0])
588
+ if old_config:
589
+ old_action = (
590
+ old_config.levelup_action
591
+ if current[1] == "levelup"
592
+ else old_config.leveldown_action
593
+ )
594
+ if old_action:
595
+ self._conbus_protocol.send_raw_telegram(f"{old_action}B")
596
+
597
+ # Send Make for new action
598
+ self._conbus_protocol.send_raw_telegram(f"{action}M")
599
+ self._active_level_action = (accessory_id, action_type)
600
+ direction = "+" if action_type == "levelup" else "-"
601
+ self.on_status_message.emit(f"Level{direction} {name} [M]")
602
+ return True
447
603
 
448
604
  def refresh_all(self) -> None:
449
605
  """
@@ -41,6 +41,8 @@ class HomekitApp(App[None]):
41
41
  ("minus", "turn_off_selected", "Off"),
42
42
  ("plus", "dim_up", "Dim+"),
43
43
  ("quotation_mark", "dim_down", "Dim-"),
44
+ ("asterisk", "level_up", "Level+"),
45
+ ("ç", "level_down", "Level-"),
44
46
  ]
45
47
 
46
48
  def __init__(self, homekit_service: HomekitService) -> None:
@@ -106,12 +108,17 @@ class HomekitApp(App[None]):
106
108
  - - : Turn OFF
107
109
  - + : Dim up
108
110
  - " : Dim down
111
+ - * : Level up
112
+ - ç : Level down
109
113
 
110
114
  Args:
111
115
  event: Key press event.
112
116
  """
113
117
  key = event.key
114
118
 
119
+ # Debug: show received key
120
+ self.homekit_service.on_status_message.emit(f"Key: {key}")
121
+
115
122
  # Selection keys (a-z0-9)
116
123
  if len(key) == 1 and (("a" <= key <= "z") or ("0" <= key <= "9")):
117
124
  accessory_id = self.homekit_service.select_accessory(key)
@@ -140,6 +147,12 @@ class HomekitApp(App[None]):
140
147
  elif key in ("quotation_mark", '"'):
141
148
  self.homekit_service.decrease_dimmer(self.selected_accessory_id)
142
149
  event.prevent_default()
150
+ elif key in ("asterisk", "star", "*"):
151
+ self.homekit_service.levelup_selected(self.selected_accessory_id)
152
+ event.prevent_default()
153
+ elif key in ("cedille", "ç"):
154
+ self.homekit_service.leveldown_selected(self.selected_accessory_id)
155
+ event.prevent_default()
143
156
 
144
157
  def _select_row(self, action_key: str) -> None:
145
158
  """
@@ -243,6 +256,16 @@ class HomekitApp(App[None]):
243
256
  if self.selected_accessory_id:
244
257
  self.homekit_service.decrease_dimmer(self.selected_accessory_id)
245
258
 
259
+ def action_level_up(self) -> None:
260
+ """Increase level on selected accessory."""
261
+ if self.selected_accessory_id:
262
+ self.homekit_service.levelup_selected(self.selected_accessory_id)
263
+
264
+ def action_level_down(self) -> None:
265
+ """Decrease level on selected accessory."""
266
+ if self.selected_accessory_id:
267
+ self.homekit_service.leveldown_selected(self.selected_accessory_id)
268
+
246
269
  async def on_unmount(self) -> None:
247
270
  """Stop AccessoryDriver and clean up service when app unmounts."""
248
271
  await self.homekit_service.stop()
@@ -398,7 +398,7 @@ class TestActionTableSerializerPadding:
398
398
  assert result[0] == 0x02 # CP20 (value=2) in BCD
399
399
  assert result[1] == 0x01 # link_number
400
400
  assert result[2] == 0x02 # module_input
401
- assert result[3] == 0x0B # output (3) | command (1 << 3)
401
+ assert result[3] == 0x0A # output 0-indexed: (3-1) | (ON << 3) = 2 | 8 = 10
402
402
  assert result[4] == 0x04 # parameter
403
403
 
404
404
  # Rest should be padding
@@ -414,3 +414,113 @@ class TestActionTableSerializerPadding:
414
414
  # Should still be 480 bytes, all zeros
415
415
  assert len(result) == 480
416
416
  assert result == b"\x00" * 480
417
+
418
+ def test_to_encoded_string_cp20_link4_input0_output1_on(self):
419
+ """
420
+ Test encoding CP20 4 0 > 1 ON produces expected BCD string.
421
+
422
+ ActionTable: CP20 4 0 > 1 ON;
423
+ Serialized BCD (first 8 chars): ACAEAAAI
424
+ """
425
+ action_table = ActionTable(
426
+ entries=[
427
+ ActionTableEntry(
428
+ module_type=ModuleTypeCode.CP20,
429
+ link_number=4,
430
+ module_input=0,
431
+ module_output=1,
432
+ command=InputActionType.ON,
433
+ parameter=TimeParam.NONE,
434
+ inverted=False,
435
+ )
436
+ ]
437
+ )
438
+
439
+ encoded_string = ActionTableSerializer.to_encoded_string(action_table)
440
+
441
+ # First 8 characters (4 bytes in BCD, high-nibble first):
442
+ # AC = 0x02 (CP20, value=2)
443
+ # AE = 0x04 (link_number=4)
444
+ # AA = 0x00 (module_input=0)
445
+ # AI = 0x08 (output 0-indexed: (1-1) | (ON<<3) = 0 | 8 = 8)
446
+ assert encoded_string[:8] == "ACAEAAAI"
447
+
448
+ def test_from_encoded_string_cp20_link4_input0_output1_on(self):
449
+ """
450
+ Test decoding BCD string ACAEAAAI produces expected ActionTable.
451
+
452
+ Serialized BCD: ACAEAAAI (+ padding)
453
+ ActionTable: CP20 4 0 > 1 ON;
454
+ """
455
+ # Build full 960-char encoded string (96 entries × 5 bytes × 2 nibbles)
456
+ # First entry: ACAEAAAI, rest is padding (AA = 0x00)
457
+ encoded_string = "ACAEAAAI" + "AA" * 476
458
+
459
+ action_table = ActionTableSerializer.from_encoded_string(encoded_string)
460
+
461
+ # Should have exactly 1 entry (padding entries with NOMOD are filtered out)
462
+ assert len(action_table.entries) == 1
463
+
464
+ entry = action_table.entries[0]
465
+ assert entry.module_type == ModuleTypeCode.CP20
466
+ assert entry.link_number == 4
467
+ assert entry.module_input == 0
468
+ assert entry.module_output == 1 # 0-indexed in wire format, converted to 1
469
+ assert entry.command == InputActionType.ON
470
+ assert entry.parameter == TimeParam.NONE
471
+ assert entry.inverted is False
472
+
473
+ def test_to_encoded_string_cp20_link13_input9_output1_levelinc(self):
474
+ """
475
+ Test encoding CP20 13 9 > 1 LEVELINC produces expected BCD string.
476
+
477
+ ActionTable: CP20 13 9 > 1 LEVELINC;
478
+ Serialized BCD (first 8 chars): ACBDAJEI
479
+ """
480
+ action_table = ActionTable(
481
+ entries=[
482
+ ActionTableEntry(
483
+ module_type=ModuleTypeCode.CP20,
484
+ link_number=13,
485
+ module_input=9,
486
+ module_output=1,
487
+ command=InputActionType.LEVELINC,
488
+ parameter=TimeParam.NONE,
489
+ inverted=False,
490
+ )
491
+ ]
492
+ )
493
+
494
+ encoded_string = ActionTableSerializer.to_encoded_string(action_table)
495
+
496
+ # First 8 characters (4 bytes in BCD, high-nibble first):
497
+ # AC = 0x02 (CP20, value=2)
498
+ # BD = 0x13 (link_number=13 in BCD)
499
+ # AJ = 0x09 (module_input=9)
500
+ # EI = 0x48 (output 0-indexed: (1-1) | (LEVELINC<<3) = 0 | 72 = 0x48)
501
+ assert encoded_string[:8] == "ACBDAJEI"
502
+
503
+ def test_from_encoded_string_cp20_link13_input9_output1_levelinc(self):
504
+ """
505
+ Test decoding BCD string ACBDAJEIAA produces expected ActionTable.
506
+
507
+ Serialized BCD: ACBDAJEIAA (+ padding)
508
+ ActionTable: CP20 13 9 > 1 LEVELINC;
509
+ """
510
+ # Build full 960-char encoded string (96 entries × 5 bytes × 2 nibbles)
511
+ # First entry: ACBDAJEIAA, rest is padding (AA = 0x00)
512
+ encoded_string = "ACBDAJEIAA" + "AA" * 475
513
+
514
+ action_table = ActionTableSerializer.from_encoded_string(encoded_string)
515
+
516
+ # Should have exactly 1 entry (padding entries with NOMOD are filtered out)
517
+ assert len(action_table.entries) == 1
518
+
519
+ entry = action_table.entries[0]
520
+ assert entry.module_type == ModuleTypeCode.CP20
521
+ assert entry.link_number == 13
522
+ assert entry.module_input == 9
523
+ assert entry.module_output == 1 # 0-indexed in wire format, converted to 1
524
+ assert entry.command == InputActionType.LEVELINC
525
+ assert entry.parameter == TimeParam.NONE
526
+ assert entry.inverted is False
@@ -408,6 +408,7 @@ class TestActionTableUploadChunkPrefix:
408
408
  mock_reply = Mock()
409
409
  mock_reply.system_function = SystemFunction.ACK
410
410
 
411
+ # First ACK after all chunks sent triggers EOF
411
412
  service._handle_upload_response(mock_reply)
412
413
 
413
414
  # Should send EOF
@@ -418,6 +419,9 @@ class TestActionTableUploadChunkPrefix:
418
419
  data_value="00",
419
420
  )
420
421
 
422
+ # Second ACK (after EOF) triggers finish signal
423
+ service._handle_upload_response(mock_reply)
424
+
421
425
  # Should call finish signal with True
422
426
  mock_finish.assert_called_once_with(True)
423
427
 
@@ -580,11 +584,11 @@ class TestActionTableUploadFullSequence:
580
584
  # Simulate connection made
581
585
  service.connection_made()
582
586
 
583
- # Simulate ACK responses for each chunk + final ACK to trigger EOF
587
+ # Simulate ACK responses for each chunk + ACK to trigger EOF + ACK after EOF
584
588
  mock_ack = Mock()
585
589
  mock_ack.system_function = SystemFunction.ACK
586
590
 
587
- for _ in range(16): # 15 chunks + 1 final ACK to trigger EOF
591
+ for _ in range(17): # 15 chunks + 1 ACK to trigger EOF + 1 ACK after EOF
588
592
  service._handle_upload_response(mock_ack)
589
593
 
590
594
  # Verify: Exactly 17 telegrams sent (1 UPLOAD_ACTIONTABLE + 15 ACTIONTABLE + 1 EOF)
File without changes
File without changes