boneIO 1.6.0.dev33__py3-none-any.whl → 1.6.0.dev35__py3-none-any.whl

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 (379) hide show
  1. boneio/components/sensor/virtual_energy.py +31 -19
  2. boneio/core/cloud/registration.py +15 -10
  3. boneio/core/config/provenance.py +7 -4
  4. boneio/core/containers.py +66 -2
  5. boneio/core/events/bus.py +8 -2
  6. boneio/core/manager/manager.py +11 -6
  7. boneio/core/messaging/basic.py +6 -0
  8. boneio/core/messaging/composite.py +4 -0
  9. boneio/core/messaging/mqtt.py +6 -0
  10. boneio/core/proxy_switch.py +149 -0
  11. boneio/core/security/certificate.py +37 -0
  12. boneio/core/system_ops.py +10 -0
  13. boneio/{example_config → factory_config/0.8}/24x16/config.yaml +11 -6
  14. boneio/{example_config → factory_config/0.8}/24x16/mqtt.yaml +1 -1
  15. boneio/factory_config/0.8/24x16/secrets.yaml +4 -0
  16. boneio/{example_config → factory_config/0.8}/32x10/config.yaml +10 -4
  17. boneio/{example_config → factory_config/0.8}/32x10/mqtt.yaml +1 -1
  18. boneio/factory_config/0.8/32x10/secrets.yaml +4 -0
  19. boneio/{example_config → factory_config/0.8}/48x4/config.yaml +10 -4
  20. boneio/{example_config → factory_config/0.8}/48x4/mqtt.yaml +1 -1
  21. boneio/factory_config/0.8/48x4/secrets.yaml +4 -0
  22. boneio/factory_config/0.8/base/config.yaml +42 -0
  23. boneio/{example_config/cover → factory_config/0.8/base}/mqtt.yaml +1 -1
  24. boneio/factory_config/0.8/base/secrets.yaml +4 -0
  25. boneio/{example_config → factory_config/0.8}/cover/config.yaml +11 -5
  26. boneio/factory_config/0.8/cover/mqtt.yaml +6 -0
  27. boneio/factory_config/0.8/cover/secrets.yaml +4 -0
  28. boneio/{example_config → factory_config/0.8}/cover_mix/config.yaml +11 -5
  29. boneio/factory_config/0.8/cover_mix/mqtt.yaml +6 -0
  30. boneio/factory_config/0.8/cover_mix/secrets.yaml +4 -0
  31. boneio/factory_config/1.0/24x16/adc.yaml +14 -0
  32. boneio/factory_config/1.0/24x16/binary_sensor.yaml +17 -0
  33. boneio/factory_config/1.0/24x16/config.yaml +48 -0
  34. boneio/factory_config/1.0/24x16/event.yaml +208 -0
  35. boneio/factory_config/1.0/24x16/mqtt.yaml +6 -0
  36. boneio/factory_config/1.0/24x16/output24x16A.yaml +96 -0
  37. boneio/factory_config/1.0/24x16/secrets.yaml +4 -0
  38. boneio/factory_config/1.0/32x10/adc.yaml +14 -0
  39. boneio/factory_config/1.0/32x10/binary_sensor.yaml +17 -0
  40. boneio/factory_config/1.0/32x10/config.yaml +48 -0
  41. boneio/factory_config/1.0/32x10/event.yaml +243 -0
  42. boneio/factory_config/1.0/32x10/mqtt.yaml +6 -0
  43. boneio/factory_config/1.0/32x10/output32x10A.yaml +128 -0
  44. boneio/factory_config/1.0/32x10/secrets.yaml +4 -0
  45. boneio/factory_config/1.0/48x4/adc.yaml +14 -0
  46. boneio/factory_config/1.0/48x4/binary_sensor.yaml +17 -0
  47. boneio/factory_config/1.0/48x4/config.yaml +48 -0
  48. boneio/factory_config/1.0/48x4/event.yaml +300 -0
  49. boneio/factory_config/1.0/48x4/mqtt.yaml +6 -0
  50. boneio/factory_config/1.0/48x4/output48x4A.yaml +192 -0
  51. boneio/factory_config/1.0/48x4/secrets.yaml +4 -0
  52. boneio/factory_config/1.0/base/config.yaml +42 -0
  53. boneio/factory_config/1.0/base/mqtt.yaml +6 -0
  54. boneio/factory_config/1.0/base/secrets.yaml +4 -0
  55. boneio/factory_config/1.0/cover/adc.yaml +14 -0
  56. boneio/factory_config/1.0/cover/binary_sensor.yaml +19 -0
  57. boneio/factory_config/1.0/cover/config.yaml +49 -0
  58. boneio/factory_config/1.0/cover/cover.yaml +112 -0
  59. boneio/factory_config/1.0/cover/event.yaml +244 -0
  60. boneio/factory_config/1.0/cover/mqtt.yaml +6 -0
  61. boneio/factory_config/1.0/cover/outputCover.yaml +65 -0
  62. boneio/factory_config/1.0/cover/secrets.yaml +4 -0
  63. boneio/factory_config/1.0/cover_mix/adc.yaml +14 -0
  64. boneio/factory_config/1.0/cover_mix/binary_sensor.yaml +19 -0
  65. boneio/factory_config/1.0/cover_mix/config.yaml +49 -0
  66. boneio/factory_config/1.0/cover_mix/cover.yaml +56 -0
  67. boneio/factory_config/1.0/cover_mix/event.yaml +242 -0
  68. boneio/factory_config/1.0/cover_mix/mqtt.yaml +6 -0
  69. boneio/factory_config/1.0/cover_mix/outputCoverMix.yaml +81 -0
  70. boneio/factory_config/1.0/cover_mix/secrets.yaml +4 -0
  71. boneio/factory_config/1.0/tester/adc.yaml +14 -0
  72. boneio/factory_config/1.0/tester/binary_sensor.yaml +17 -0
  73. boneio/factory_config/1.0/tester/config.yaml +49 -0
  74. boneio/factory_config/1.0/tester/event.yaml +243 -0
  75. boneio/factory_config/1.0/tester/mqtt.yaml +6 -0
  76. boneio/factory_config/1.0/tester/output32x10A.yaml +128 -0
  77. boneio/factory_config/1.0/tester/secrets.yaml +4 -0
  78. boneio/factory_config/1.1/24x16/adc.yaml +14 -0
  79. boneio/factory_config/1.1/24x16/binary_sensor.yaml +17 -0
  80. boneio/factory_config/1.1/24x16/config.yaml +48 -0
  81. boneio/factory_config/1.1/24x16/event.yaml +208 -0
  82. boneio/factory_config/1.1/24x16/mqtt.yaml +6 -0
  83. boneio/factory_config/1.1/24x16/output24x16A.yaml +96 -0
  84. boneio/factory_config/1.1/24x16/secrets.yaml +4 -0
  85. boneio/factory_config/1.1/32x10/adc.yaml +14 -0
  86. boneio/factory_config/1.1/32x10/binary_sensor.yaml +17 -0
  87. boneio/factory_config/1.1/32x10/config.yaml +48 -0
  88. boneio/factory_config/1.1/32x10/event.yaml +243 -0
  89. boneio/factory_config/1.1/32x10/mqtt.yaml +6 -0
  90. boneio/factory_config/1.1/32x10/output32x10A.yaml +128 -0
  91. boneio/factory_config/1.1/32x10/secrets.yaml +4 -0
  92. boneio/factory_config/1.1/48x4/adc.yaml +14 -0
  93. boneio/factory_config/1.1/48x4/binary_sensor.yaml +17 -0
  94. boneio/factory_config/1.1/48x4/config.yaml +48 -0
  95. boneio/factory_config/1.1/48x4/event.yaml +300 -0
  96. boneio/factory_config/1.1/48x4/mqtt.yaml +6 -0
  97. boneio/factory_config/1.1/48x4/output48x4A.yaml +192 -0
  98. boneio/factory_config/1.1/48x4/secrets.yaml +4 -0
  99. boneio/factory_config/1.1/base/config.yaml +42 -0
  100. boneio/factory_config/1.1/base/mqtt.yaml +6 -0
  101. boneio/factory_config/1.1/base/secrets.yaml +4 -0
  102. boneio/factory_config/1.1/cover/adc.yaml +14 -0
  103. boneio/factory_config/1.1/cover/binary_sensor.yaml +19 -0
  104. boneio/factory_config/1.1/cover/config.yaml +49 -0
  105. boneio/factory_config/1.1/cover/cover.yaml +112 -0
  106. boneio/factory_config/1.1/cover/event.yaml +244 -0
  107. boneio/factory_config/1.1/cover/mqtt.yaml +6 -0
  108. boneio/factory_config/1.1/cover/outputCover.yaml +65 -0
  109. boneio/factory_config/1.1/cover/secrets.yaml +4 -0
  110. boneio/factory_config/1.1/cover_mix/adc.yaml +14 -0
  111. boneio/factory_config/1.1/cover_mix/binary_sensor.yaml +19 -0
  112. boneio/factory_config/1.1/cover_mix/config.yaml +49 -0
  113. boneio/factory_config/1.1/cover_mix/cover.yaml +56 -0
  114. boneio/factory_config/1.1/cover_mix/event.yaml +242 -0
  115. boneio/factory_config/1.1/cover_mix/mqtt.yaml +6 -0
  116. boneio/factory_config/1.1/cover_mix/outputCoverMix.yaml +81 -0
  117. boneio/factory_config/1.1/cover_mix/secrets.yaml +4 -0
  118. boneio/factory_config/1.1/tester/adc.yaml +14 -0
  119. boneio/factory_config/1.1/tester/binary_sensor.yaml +17 -0
  120. boneio/factory_config/1.1/tester/config.yaml +49 -0
  121. boneio/factory_config/1.1/tester/event.yaml +243 -0
  122. boneio/factory_config/1.1/tester/mqtt.yaml +6 -0
  123. boneio/factory_config/1.1/tester/output32x10A.yaml +128 -0
  124. boneio/factory_config/1.1/tester/secrets.yaml +4 -0
  125. boneio/factory_config/__init__.py +41 -0
  126. boneio/migrations/assets/MANIFEST.sha256 +13 -4
  127. boneio/migrations/assets/apt/53boneio-caddy +17 -0
  128. boneio/migrations/assets/apt/caddy-pin.pref +3 -0
  129. boneio/migrations/assets/apt/caddy-stable-archive-keyring.gpg +0 -0
  130. boneio/migrations/assets/apt/caddy-stable.list +1 -0
  131. boneio/migrations/assets/caddy/502.html +35 -0
  132. boneio/migrations/assets/docker/nodered/caddy/502.html +22 -30
  133. boneio/migrations/assets/docker/nodered/docker-compose-native-proxy.yaml +23 -0
  134. boneio/migrations/assets/helpers/boneio-containers +1134 -38
  135. boneio/migrations/assets/helpers/boneio-helpers-heal +17 -2
  136. boneio/migrations/assets/helpers/boneio-proxy-config +677 -0
  137. boneio/migrations/assets/helpers/boneio-system +118 -1
  138. boneio/migrations/assets/systemd/boneio-proxy-recover.service +24 -0
  139. boneio/migrations/assets/systemd/caddy-boneio.conf +44 -0
  140. boneio/migrations/plans/1.4.4.json +1 -1
  141. boneio/migrations/plans/1.4.4.sig +0 -0
  142. boneio/migrations/plans/1.6.17.json +1 -1
  143. boneio/migrations/plans/1.6.17.sig +1 -1
  144. boneio/migrations/plans/1.6.18.json +1 -1
  145. boneio/migrations/plans/1.6.18.sig +1 -1
  146. boneio/migrations/plans/1.6.21.json +1 -1
  147. boneio/migrations/plans/1.6.21.sig +1 -1
  148. boneio/migrations/plans/1.6.22.json +1 -1
  149. boneio/migrations/plans/1.6.22.sig +1 -3
  150. boneio/migrations/plans/1.6.26.json +1 -1
  151. boneio/migrations/plans/1.6.26.sig +1 -1
  152. boneio/migrations/plans/1.6.27.json +1 -1
  153. boneio/migrations/plans/1.6.27.sig +1 -1
  154. boneio/migrations/plans/1.6.29.json +1 -1
  155. boneio/migrations/plans/1.6.29.sig +1 -1
  156. boneio/migrations/plans/1.6.30.json +1 -1
  157. boneio/migrations/plans/1.6.30.sig +1 -1
  158. boneio/migrations/plans/1.6.31.json +1 -1
  159. boneio/migrations/plans/1.6.31.sig +1 -1
  160. boneio/migrations/plans/1.6.32.json +1 -1
  161. boneio/migrations/plans/1.6.32.sig +2 -1
  162. boneio/migrations/plans/1.6.33.json +1 -1
  163. boneio/migrations/plans/1.6.33.sig +1 -1
  164. boneio/migrations/plans/1.6.39.json +1 -0
  165. boneio/migrations/plans/1.6.39.sig +1 -0
  166. boneio/migrations/plans/1.6.40.json +1 -0
  167. boneio/migrations/plans/1.6.40.sig +1 -0
  168. boneio/migrations/plans/1.6.41.json +1 -0
  169. boneio/migrations/plans/1.6.41.sig +2 -0
  170. boneio/migrations/plans/1.6.42.json +1 -0
  171. boneio/migrations/plans/1.6.42.sig +1 -0
  172. boneio/migrations/plans/1.6.43.json +1 -0
  173. boneio/migrations/plans/1.6.43.sig +0 -0
  174. boneio/migrations/plans/1.6.5.json +1 -1
  175. boneio/migrations/plans/1.6.5.sig +1 -2
  176. boneio/migrations/plans/1.6.8.json +1 -1
  177. boneio/migrations/plans/1.6.8.sig +0 -0
  178. boneio/migrations/plans/1.6.9.json +1 -1
  179. boneio/migrations/plans/1.6.9.sig +1 -1
  180. boneio/migrations/plans/manifest.json +1 -1
  181. boneio/migrations/plans/manifest.sig +1 -1
  182. boneio/migrations/versions/v1_6_39_containers_pull_timeout.py +41 -0
  183. boneio/migrations/versions/v1_6_40_disk_usage.py +43 -0
  184. boneio/migrations/versions/v1_6_41_starting_page.py +29 -0
  185. boneio/migrations/versions/v1_6_42_native_caddy_staging.py +140 -0
  186. boneio/migrations/versions/v1_6_43_log2ram_cp.py +34 -0
  187. boneio/runner.py +8 -1
  188. boneio/schema/schema.yaml +4 -1
  189. boneio/version.py +1 -1
  190. boneio/webui/app.py +3 -0
  191. boneio/webui/bind.py +63 -3
  192. boneio/webui/frontend-dist/assets/{AccountsView-Cbqd4Ao_.js → AccountsView-AMfOhw42.js} +1 -1
  193. boneio/webui/frontend-dist/assets/AccountsView-AMfOhw42.js.gz +0 -0
  194. boneio/webui/frontend-dist/assets/{AppInitContext-DWvK3qS_.js → AppInitContext-BDF1KqeY.js} +1 -1
  195. boneio/webui/frontend-dist/assets/AppInitContext-BDF1KqeY.js.gz +0 -0
  196. boneio/webui/frontend-dist/assets/{BackupSection-8QSuOR0c.js → BackupSection-D7XzmmGa.js} +1 -1
  197. boneio/webui/frontend-dist/assets/BackupSection-D7XzmmGa.js.gz +0 -0
  198. boneio/webui/frontend-dist/assets/{BindingMatrix-CfzTlbKI.js → BindingMatrix-Dm7enV81.js} +1 -1
  199. boneio/webui/frontend-dist/assets/BindingMatrix-Dm7enV81.js.gz +0 -0
  200. boneio/webui/frontend-dist/assets/{DeviceControlSection-BDnXyBJT.js → DeviceControlSection-CbHya4k0.js} +1 -1
  201. boneio/webui/frontend-dist/assets/DeviceControlSection-CbHya4k0.js.gz +0 -0
  202. boneio/webui/frontend-dist/assets/DiagnosticsView-BiF5br1t.js +5 -0
  203. boneio/webui/frontend-dist/assets/DiagnosticsView-BiF5br1t.js.gz +0 -0
  204. boneio/webui/frontend-dist/assets/{EntityGrid-DaGv-7ct.js → EntityGrid-4a1Dbqth.js} +1 -1
  205. boneio/webui/frontend-dist/assets/EntityGrid-4a1Dbqth.js.gz +0 -0
  206. boneio/webui/frontend-dist/assets/FactoryResetSection-Dsi68Z8u.js +1 -0
  207. boneio/webui/frontend-dist/assets/FactoryResetSection-Dsi68Z8u.js.gz +0 -0
  208. boneio/webui/frontend-dist/assets/FormInputToggle-Buzoogqy.js +11 -0
  209. boneio/webui/frontend-dist/assets/FormInputToggle-Buzoogqy.js.gz +0 -0
  210. boneio/webui/frontend-dist/assets/{HaDashboardWizard-B4KWLmIy.js → HaDashboardWizard-AD5uYoaQ.js} +1 -1
  211. boneio/webui/frontend-dist/assets/HaDashboardWizard-AD5uYoaQ.js.gz +0 -0
  212. boneio/webui/frontend-dist/assets/{HostnameSection-IfbaLobN.js → HostnameSection-tiU6n56p.js} +1 -1
  213. boneio/webui/frontend-dist/assets/HostnameSection-tiU6n56p.js.gz +0 -0
  214. boneio/webui/frontend-dist/assets/InputsView-D4MA7EqS.js +1 -0
  215. boneio/webui/frontend-dist/assets/InputsView-D4MA7EqS.js.gz +0 -0
  216. boneio/webui/frontend-dist/assets/{LoadingState-Bp0pGOdE.js → LoadingState-CxmjkMgn.js} +1 -1
  217. boneio/webui/frontend-dist/assets/{LocationSection-CEDOjibw.js → LocationSection-BgvLiPAD.js} +2 -2
  218. boneio/webui/frontend-dist/assets/LocationSection-BgvLiPAD.js.gz +0 -0
  219. boneio/webui/frontend-dist/assets/LoggerForm-Be95erZp.js +1 -0
  220. boneio/webui/frontend-dist/assets/LoggerForm-Be95erZp.js.gz +0 -0
  221. boneio/webui/frontend-dist/assets/{LongPressWrapper-B9Hl3ltV.js → LongPressWrapper-C266DAt0.js} +1 -1
  222. boneio/webui/frontend-dist/assets/LongPressWrapper-C266DAt0.js.gz +0 -0
  223. boneio/webui/frontend-dist/assets/{MigrationsSection-D3xuvZyO.js → MigrationsSection-Dg3dgGBw.js} +1 -1
  224. boneio/webui/frontend-dist/assets/MigrationsSection-Dg3dgGBw.js.gz +0 -0
  225. boneio/webui/frontend-dist/assets/{ModbusView-Bg5HDFqc.js → ModbusView-g7kLvU1N.js} +1 -1
  226. boneio/webui/frontend-dist/assets/ModbusView-g7kLvU1N.js.gz +0 -0
  227. boneio/webui/frontend-dist/assets/{MqttPasswordsSection-CMVKRHVp.js → MqttPasswordsSection-9qBygV7e.js} +1 -1
  228. boneio/webui/frontend-dist/assets/MqttPasswordsSection-9qBygV7e.js.gz +0 -0
  229. boneio/webui/frontend-dist/assets/NodeRedManagement-CaMi1mNV.js +1 -0
  230. boneio/webui/frontend-dist/assets/NodeRedManagement-CaMi1mNV.js.gz +0 -0
  231. boneio/webui/frontend-dist/assets/{NodeRedView-CP86NnC4.js → NodeRedView-BZdmAtAt.js} +1 -1
  232. boneio/webui/frontend-dist/assets/NodeRedView-BZdmAtAt.js.gz +0 -0
  233. boneio/webui/frontend-dist/assets/{NumericInput-CrVNlFps.js → NumericInput-fo5QZMhp.js} +1 -1
  234. boneio/webui/frontend-dist/assets/NumericInput-fo5QZMhp.js.gz +0 -0
  235. boneio/webui/frontend-dist/assets/OutputsView-C-HNqu4_.js +1 -0
  236. boneio/webui/frontend-dist/assets/OutputsView-C-HNqu4_.js.gz +0 -0
  237. boneio/webui/frontend-dist/assets/{SecurityView-C8VMMZ2Q.js → SecurityView-Bj3K9I1b.js} +1 -1
  238. boneio/webui/frontend-dist/assets/SecurityView-Bj3K9I1b.js.gz +0 -0
  239. boneio/webui/frontend-dist/assets/{SensorView-DMh2GK_I.js → SensorView-CgO65fQs.js} +1 -1
  240. boneio/webui/frontend-dist/assets/SensorView-CgO65fQs.js.gz +0 -0
  241. boneio/webui/frontend-dist/assets/{Sparkline-6iDv_8hd.js → Sparkline-DoHMYvvZ.js} +2 -2
  242. boneio/webui/frontend-dist/assets/Sparkline-DoHMYvvZ.js.gz +0 -0
  243. boneio/webui/frontend-dist/assets/SystemState-Cm6R3WLd.js +2 -0
  244. boneio/webui/frontend-dist/assets/SystemState-Cm6R3WLd.js.gz +0 -0
  245. boneio/webui/frontend-dist/assets/TemplatesView-DZVBJt5J.js +1 -0
  246. boneio/webui/frontend-dist/assets/TemplatesView-DZVBJt5J.js.gz +0 -0
  247. boneio/webui/frontend-dist/assets/{TilePicker-DZrA7qwM.js → TilePicker-BkLaQi0H.js} +1 -1
  248. boneio/webui/frontend-dist/assets/TilePicker-BkLaQi0H.js.gz +0 -0
  249. boneio/webui/frontend-dist/assets/{TimezoneSection-CA36pxfJ.js → TimezoneSection-B0y1rM9Q.js} +1 -1
  250. boneio/webui/frontend-dist/assets/TimezoneSection-B0y1rM9Q.js.gz +0 -0
  251. boneio/webui/frontend-dist/assets/UISettings-DihZLsQT.js +102 -0
  252. boneio/webui/frontend-dist/assets/UISettings-DihZLsQT.js.gz +0 -0
  253. boneio/webui/frontend-dist/assets/{UpdateSafetyNotice-CAKxJzbp.js → UpdateSafetyNotice-Mp0jJZ1F.js} +1 -1
  254. boneio/webui/frontend-dist/assets/UpdateSafetyNotice-Mp0jJZ1F.js.gz +0 -0
  255. boneio/webui/frontend-dist/assets/{ViewToggle-CVWoIKqI.js → ViewToggle-CUtK4zn3.js} +1 -1
  256. boneio/webui/frontend-dist/assets/{aiConfig-BB8qvNZ5.js → aiConfig-C5JZrzmG.js} +1 -1
  257. boneio/webui/frontend-dist/assets/aiConfig-C5JZrzmG.js.gz +0 -0
  258. boneio/webui/frontend-dist/assets/{aiWizardPrompt-HWioCYHg.js → aiWizardPrompt-BW96nE6h.js} +1 -1
  259. boneio/webui/frontend-dist/assets/aiWizardPrompt-BW96nE6h.js.gz +0 -0
  260. boneio/webui/frontend-dist/assets/{dialog-DMaCttcv.js → dialog-DcnAJg2-.js} +1 -1
  261. boneio/webui/frontend-dist/assets/dialog-DcnAJg2-.js.gz +0 -0
  262. boneio/webui/frontend-dist/assets/{fa-ChDOCx3T.js → fa-wf69YpA7.js} +1 -1
  263. boneio/webui/frontend-dist/assets/fa-wf69YpA7.js.gz +0 -0
  264. boneio/webui/frontend-dist/assets/{gi-DiFRP1nv.js → gi-CdbYopzz.js} +1 -1
  265. boneio/webui/frontend-dist/assets/{groupByArea-DIIjg3E_.js → groupByArea-CGu58rU4.js} +1 -1
  266. boneio/webui/frontend-dist/assets/groupByArea-CGu58rU4.js.gz +0 -0
  267. boneio/webui/frontend-dist/assets/index-BiDbe4jH.css +2 -0
  268. boneio/webui/frontend-dist/assets/{index-ybynnlsQ.css.gz → index-BiDbe4jH.css.gz} +0 -0
  269. boneio/webui/frontend-dist/assets/index-CO67NAHS.js +46 -0
  270. boneio/webui/frontend-dist/assets/index-CO67NAHS.js.gz +0 -0
  271. boneio/webui/frontend-dist/assets/{monaco-CoUZsnkn.js → monaco-BNnzf817.js} +1 -1
  272. boneio/webui/frontend-dist/assets/monaco-BNnzf817.js.gz +0 -0
  273. boneio/webui/frontend-dist/assets/{monaco-BksocEXi.js → monaco-CAkAVZil.js} +3 -3
  274. boneio/webui/frontend-dist/assets/monaco-CAkAVZil.js.gz +0 -0
  275. boneio/webui/frontend-dist/assets/{select-iQAUlBVk.js → select-DsYydCvY.js} +1 -1
  276. boneio/webui/frontend-dist/assets/select-DsYydCvY.js.gz +0 -0
  277. boneio/webui/frontend-dist/index.html +5 -5
  278. boneio/webui/frontend-dist/schema/config.schema.json +1 -1
  279. boneio/webui/frontend-dist/schema/web.schema.json +1 -1
  280. boneio/webui/frontend-dist/sw.js +1 -1
  281. boneio/webui/middleware/policy.py +7 -0
  282. boneio/webui/routes/__init__.py +2 -0
  283. boneio/webui/routes/config_actions.py +6 -4
  284. boneio/webui/routes/config_core.py +75 -0
  285. boneio/webui/routes/diagnostics.py +55 -0
  286. boneio/webui/routes/migrations.py +3 -1
  287. boneio/webui/routes/nodered.py +45 -5
  288. boneio/webui/routes/onboarding.py +126 -1
  289. boneio/webui/routes/os_update.py +5 -0
  290. boneio/webui/routes/proxy.py +62 -0
  291. boneio/webui/routes/security.py +28 -11
  292. boneio/webui/routes/system.py +3 -1
  293. boneio/webui/routes/update.py +52 -21
  294. boneio/webui/schema/config.schema.json +1 -1
  295. boneio/webui/schema/web.schema.json +1 -1
  296. boneio/webui/web_server.py +88 -2
  297. boneio-1.6.0.dev35.dist-info/METADATA +109 -0
  298. {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/RECORD +324 -196
  299. boneio/example_config/__init__.py +0 -0
  300. boneio/example_config/cover_mix/mqtt.yaml +0 -6
  301. boneio/webui/frontend-dist/assets/AccountsView-Cbqd4Ao_.js.gz +0 -0
  302. boneio/webui/frontend-dist/assets/AppInitContext-DWvK3qS_.js.gz +0 -0
  303. boneio/webui/frontend-dist/assets/BackupSection-8QSuOR0c.js.gz +0 -0
  304. boneio/webui/frontend-dist/assets/BindingMatrix-CfzTlbKI.js.gz +0 -0
  305. boneio/webui/frontend-dist/assets/DeviceControlSection-BDnXyBJT.js.gz +0 -0
  306. boneio/webui/frontend-dist/assets/DiagnosticsView-BFbE2gzt.js +0 -5
  307. boneio/webui/frontend-dist/assets/DiagnosticsView-BFbE2gzt.js.gz +0 -0
  308. boneio/webui/frontend-dist/assets/EntityGrid-DaGv-7ct.js.gz +0 -0
  309. boneio/webui/frontend-dist/assets/FactoryResetSection-C-k28afU.js +0 -1
  310. boneio/webui/frontend-dist/assets/FactoryResetSection-C-k28afU.js.gz +0 -0
  311. boneio/webui/frontend-dist/assets/FormInputToggle-BgXGSDAE.js +0 -11
  312. boneio/webui/frontend-dist/assets/FormInputToggle-BgXGSDAE.js.gz +0 -0
  313. boneio/webui/frontend-dist/assets/HaDashboardWizard-B4KWLmIy.js.gz +0 -0
  314. boneio/webui/frontend-dist/assets/HostnameSection-IfbaLobN.js.gz +0 -0
  315. boneio/webui/frontend-dist/assets/InputsView-LHCSi3jQ.js +0 -1
  316. boneio/webui/frontend-dist/assets/InputsView-LHCSi3jQ.js.gz +0 -0
  317. boneio/webui/frontend-dist/assets/LocationSection-CEDOjibw.js.gz +0 -0
  318. boneio/webui/frontend-dist/assets/LoggerForm-C2G_nFBk.js +0 -1
  319. boneio/webui/frontend-dist/assets/LoggerForm-C2G_nFBk.js.gz +0 -0
  320. boneio/webui/frontend-dist/assets/LongPressWrapper-B9Hl3ltV.js.gz +0 -0
  321. boneio/webui/frontend-dist/assets/MigrationsSection-D3xuvZyO.js.gz +0 -0
  322. boneio/webui/frontend-dist/assets/ModbusView-Bg5HDFqc.js.gz +0 -0
  323. boneio/webui/frontend-dist/assets/MqttPasswordsSection-CMVKRHVp.js.gz +0 -0
  324. boneio/webui/frontend-dist/assets/NodeRedManagement-8LQGoZFd.js +0 -1
  325. boneio/webui/frontend-dist/assets/NodeRedManagement-8LQGoZFd.js.gz +0 -0
  326. boneio/webui/frontend-dist/assets/NodeRedView-CP86NnC4.js.gz +0 -0
  327. boneio/webui/frontend-dist/assets/NumericInput-CrVNlFps.js.gz +0 -0
  328. boneio/webui/frontend-dist/assets/OutputsView-CkgUuZDI.js +0 -1
  329. boneio/webui/frontend-dist/assets/OutputsView-CkgUuZDI.js.gz +0 -0
  330. boneio/webui/frontend-dist/assets/SecurityView-C8VMMZ2Q.js.gz +0 -0
  331. boneio/webui/frontend-dist/assets/SensorView-DMh2GK_I.js.gz +0 -0
  332. boneio/webui/frontend-dist/assets/Sparkline-6iDv_8hd.js.gz +0 -0
  333. boneio/webui/frontend-dist/assets/SystemState-DUOSa8P5.js +0 -2
  334. boneio/webui/frontend-dist/assets/SystemState-DUOSa8P5.js.gz +0 -0
  335. boneio/webui/frontend-dist/assets/TemplatesView-DVP3Ifd4.js +0 -1
  336. boneio/webui/frontend-dist/assets/TemplatesView-DVP3Ifd4.js.gz +0 -0
  337. boneio/webui/frontend-dist/assets/TilePicker-DZrA7qwM.js.gz +0 -0
  338. boneio/webui/frontend-dist/assets/TimezoneSection-CA36pxfJ.js.gz +0 -0
  339. boneio/webui/frontend-dist/assets/UISettings-CQ64K21V.js +0 -102
  340. boneio/webui/frontend-dist/assets/UISettings-CQ64K21V.js.gz +0 -0
  341. boneio/webui/frontend-dist/assets/UpdateSafetyNotice-CAKxJzbp.js.gz +0 -0
  342. boneio/webui/frontend-dist/assets/aiConfig-BB8qvNZ5.js.gz +0 -0
  343. boneio/webui/frontend-dist/assets/aiWizardPrompt-HWioCYHg.js.gz +0 -0
  344. boneio/webui/frontend-dist/assets/dialog-DMaCttcv.js.gz +0 -0
  345. boneio/webui/frontend-dist/assets/fa-ChDOCx3T.js.gz +0 -0
  346. boneio/webui/frontend-dist/assets/groupByArea-DIIjg3E_.js.gz +0 -0
  347. boneio/webui/frontend-dist/assets/index-Co3agMvu.js +0 -42
  348. boneio/webui/frontend-dist/assets/index-Co3agMvu.js.gz +0 -0
  349. boneio/webui/frontend-dist/assets/index-ybynnlsQ.css +0 -2
  350. boneio/webui/frontend-dist/assets/monaco-BksocEXi.js.gz +0 -0
  351. boneio/webui/frontend-dist/assets/monaco-CoUZsnkn.js.gz +0 -0
  352. boneio/webui/frontend-dist/assets/select-iQAUlBVk.js.gz +0 -0
  353. boneio-1.6.0.dev33.dist-info/METADATA +0 -69
  354. /boneio/{example_config → factory_config/0.8}/24x16/adc.yaml +0 -0
  355. /boneio/{example_config → factory_config/0.8}/24x16/binary_sensor.yaml +0 -0
  356. /boneio/{example_config → factory_config/0.8}/24x16/event.yaml +0 -0
  357. /boneio/{example_config → factory_config/0.8}/24x16/output24x16A.yaml +0 -0
  358. /boneio/{example_config → factory_config/0.8}/32x10/adc.yaml +0 -0
  359. /boneio/{example_config → factory_config/0.8}/32x10/binary_sensor.yaml +0 -0
  360. /boneio/{example_config → factory_config/0.8}/32x10/event.yaml +0 -0
  361. /boneio/{example_config → factory_config/0.8}/32x10/output32x10A.yaml +0 -0
  362. /boneio/{example_config → factory_config/0.8}/48x4/adc.yaml +0 -0
  363. /boneio/{example_config → factory_config/0.8}/48x4/binary_sensor.yaml +0 -0
  364. /boneio/{example_config → factory_config/0.8}/48x4/event.yaml +0 -0
  365. /boneio/{example_config → factory_config/0.8}/48x4/output48x4A.yaml +0 -0
  366. /boneio/{example_config → factory_config/0.8}/cover/adc.yaml +0 -0
  367. /boneio/{example_config → factory_config/0.8}/cover/binary_sensor.yaml +0 -0
  368. /boneio/{example_config → factory_config/0.8}/cover/cover.yaml +0 -0
  369. /boneio/{example_config → factory_config/0.8}/cover/event.yaml +0 -0
  370. /boneio/{example_config → factory_config/0.8}/cover/outputCover.yaml +0 -0
  371. /boneio/{example_config → factory_config/0.8}/cover_mix/adc.yaml +0 -0
  372. /boneio/{example_config → factory_config/0.8}/cover_mix/binary_sensor.yaml +0 -0
  373. /boneio/{example_config → factory_config/0.8}/cover_mix/cover.yaml +0 -0
  374. /boneio/{example_config → factory_config/0.8}/cover_mix/event.yaml +0 -0
  375. /boneio/{example_config → factory_config/0.8}/cover_mix/outputCoverMix.yaml +0 -0
  376. {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/WHEEL +0 -0
  377. {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/entry_points.txt +0 -0
  378. {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/licenses/LICENSE +0 -0
  379. {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/top_level.txt +0 -0
@@ -25,6 +25,10 @@ if TYPE_CHECKING:
25
25
 
26
26
  _LOGGER = logging.getLogger(__name__)
27
27
 
28
+ # How long to wait, once the broker has the subscription, for a stored total.
29
+ # None arriving means the counter has never been published.
30
+ RESTORE_GRACE_SECONDS = 15.0
31
+
28
32
 
29
33
  class VirtualEnergySensor:
30
34
  """Virtual energy/water sensor linked to an output.
@@ -72,6 +76,7 @@ class VirtualEnergySensor:
72
76
  self._area = area
73
77
 
74
78
  self._virtual_sensors_task = None
79
+ self._restore_task: asyncio.Task | None = None
75
80
 
76
81
  # Counters
77
82
  self._energy_consumed_Wh = 0.0
@@ -145,7 +150,8 @@ class VirtualEnergySensor:
145
150
 
146
151
  def start_tracking(self):
147
152
  """Start tracking consumption (called when output turns ON)."""
148
- self._last_on_timestamp = time.time()
153
+ if self._last_on_timestamp is None: # ON while already on keeps the stretch
154
+ self._last_on_timestamp = time.time()
149
155
  if self._virtual_sensors_task is not None and not self._virtual_sensors_task.done():
150
156
  return # Already running
151
157
  self._virtual_sensors_task = self._loop.create_task(self._tracking_loop())
@@ -169,20 +175,9 @@ class VirtualEnergySensor:
169
175
  async def _tracking_loop(self):
170
176
  """Periodically update and send state every 30 seconds while output is ON."""
171
177
  try:
172
- # Wait for state restore before sending any state.
173
- # This prevents overwriting the retained MQTT value with 0.
174
- # Timeout of 5s ensures we don't hang if MQTT is down.
175
- try:
176
- await asyncio.wait_for(self._restore_done.wait(), timeout=5.0)
177
- except TimeoutError:
178
- _LOGGER.warning(
179
- "State restore timeout for %s, starting with energy=%.4f Wh", self._id, self._energy_consumed_Wh
180
- )
181
- self._restore_done.set()
182
-
183
- # Reset on_timestamp after restore so we don't count
184
- # the time elapsed during the restore wait.
185
- self._last_on_timestamp = time.time()
178
+ # Publishing before the stored total is known would replace it
179
+ # with what was counted since boot.
180
+ await self._restore_done.wait()
186
181
 
187
182
  while self._output.state == ON:
188
183
  self._update_consumption()
@@ -194,7 +189,9 @@ class VirtualEnergySensor:
194
189
  def _update_consumption(self):
195
190
  """Update consumption counters based on elapsed time."""
196
191
  now = time.time()
197
- if self._output.state == ON and self._last_on_timestamp is not None:
192
+ # Not the output's state: by the time OFF is handled it is already
193
+ # off, and the stretch since the last update would be lost.
194
+ if self._last_on_timestamp is not None:
198
195
  elapsed = now - self._last_on_timestamp
199
196
 
200
197
  if self._sensor_type == "power" and self._power_usage is not None:
@@ -215,14 +212,17 @@ class VirtualEnergySensor:
215
212
  """
216
213
 
217
214
  async def on_message(_topic, payload):
215
+ if self._restore_done.is_set():
216
+ return # Late, after giving up: the counter is already published.
218
217
  try:
219
218
  data = json.loads(payload)
220
219
  if isinstance(data, dict):
220
+ # Added, not assigned: the output may have been on since boot.
221
221
  if "energy" in data:
222
- self._energy_consumed_Wh = float(data["energy"])
222
+ self._energy_consumed_Wh += float(data["energy"])
223
223
  _LOGGER.info("Restored energy state for %s: %.4f Wh", self._id, self._energy_consumed_Wh)
224
224
  if "water" in data:
225
- self._water_consumed_L = float(data["water"])
225
+ self._water_consumed_L += float(data["water"])
226
226
  _LOGGER.info("Restored water state for %s: %.4f L", self._id, self._water_consumed_L)
227
227
  except Exception as e:
228
228
  _LOGGER.warning("Failed to restore state for %s: %s", self._id, e)
@@ -230,8 +230,20 @@ class VirtualEnergySensor:
230
230
  self._restore_done.set()
231
231
  await self._message_bus.unsubscribe_and_stop_listen(self._sensor_topic)
232
232
 
233
+ async def restore():
234
+ await self._message_bus.subscribe_and_listen(self._sensor_topic, on_message)
235
+ # The grace starts once the broker has the subscription, not at
236
+ # boot: connecting can take longer than any fixed timeout.
237
+ await self._message_bus.wait_until_subscribed()
238
+ try:
239
+ await asyncio.wait_for(self._restore_done.wait(), timeout=RESTORE_GRACE_SECONDS)
240
+ except TimeoutError:
241
+ _LOGGER.info("No stored state for %s, counting from boot", self._id)
242
+ self._restore_done.set()
243
+ await self._message_bus.unsubscribe_and_stop_listen(self._sensor_topic)
244
+
233
245
  if self._message_bus is not None:
234
- asyncio.create_task(self._message_bus.subscribe_and_listen(self._sensor_topic, on_message))
246
+ self._restore_task = asyncio.create_task(restore())
235
247
 
236
248
  def get_current_power(self) -> float:
237
249
  """Get current power usage in W (0 if output is OFF)."""
@@ -119,7 +119,13 @@ class CloudRegistration:
119
119
 
120
120
  @property
121
121
  def is_compose_writable(self) -> bool:
122
- """Check if docker-compose.yaml is writable without side effects."""
122
+ """Check if docker-compose.yaml is writable without side effects.
123
+
124
+ True for the packaged Caddy: there is no compose file to be writable,
125
+ and the panel warns when this is False.
126
+ """
127
+ if containers.proxy_mode() == "native":
128
+ return True
123
129
  compose_file = _DOCKER_DIR / "docker-compose.yaml"
124
130
  if not compose_file.exists():
125
131
  return False
@@ -521,6 +527,9 @@ class CloudRegistration:
521
527
  This ensures the script is always up-to-date after a pip upgrade,
522
528
  even if the user never re-clones the full repo.
523
529
  """
530
+ if containers.proxy_mode() == "native":
531
+ # The generator writes the cloud block itself; no script is mounted.
532
+ return
524
533
  dest = CADDY_CONFIG_DIR / "init-certs-cloud.sh"
525
534
  try:
526
535
  src = files("boneio.core.cloud.data").joinpath("init-certs-cloud.sh")
@@ -661,19 +670,15 @@ class CloudRegistration:
661
670
 
662
671
  def is_cloud_config_active(self) -> bool:
663
672
  """
664
- Check if cloud init-certs script is active in docker-compose.
673
+ Check if the cloud template is the one in use.
674
+
675
+ Asked of the compose file in container mode and of the helper's marker
676
+ for the packaged Caddy, which has no compose file.
665
677
 
666
678
  Returns:
667
679
  True if cloud config is active
668
680
  """
669
- try:
670
- compose_file = _DOCKER_DIR / "docker-compose.yaml"
671
- if not compose_file.exists():
672
- return False
673
- content = compose_file.read_text()
674
- return "init-certs-cloud.sh" in content
675
- except Exception:
676
- return False
681
+ return containers.cloud_template_is_live()
677
682
 
678
683
 
679
684
  async def set_enabled(config_helper, enabled: bool, local_ip: str | None = None) -> str:
@@ -75,9 +75,12 @@ def looks_factory_fresh(
75
75
  return True
76
76
 
77
77
  root = Path(template_dir) if template_dir is not None else FACTORY_TEMPLATE_DIR
78
- try:
79
- variants = sorted(p for p in root.iterdir() if p.is_dir())
80
- except OSError:
78
+ # <variant>/ on older images, <revision>/<variant>/ since one image serves
79
+ # every board.
80
+ variants = sorted(
81
+ p.parent for p in [*root.glob("*/config.yaml"), *root.glob("*/*/config.yaml")]
82
+ )
83
+ if not variants:
81
84
  # No templates to compare against: the honest answer is "cannot tell",
82
85
  # and the safe reading of that is "assume it has been configured".
83
86
  _LOGGER.debug("No factory templates under %s — assuming configured", root)
@@ -85,7 +88,7 @@ def looks_factory_fresh(
85
88
 
86
89
  for variant in variants:
87
90
  if current == _digest(variant / "config.yaml"):
88
- _LOGGER.debug("Configuration matches the %s template", variant.name)
91
+ _LOGGER.debug("Configuration matches the %s template", variant.relative_to(root))
89
92
  return True
90
93
 
91
94
  return False
boneio/core/containers.py CHANGED
@@ -90,6 +90,13 @@ _HELPER_ONLY = {
90
90
  # Read the root-owned template and rewrite the compose file's Caddy line.
91
91
  "caddy-image-state",
92
92
  "caddy-image-apply",
93
+ # Root reads Caddy's own copy; without the helper the panel reads it itself.
94
+ "caddy-root-ca",
95
+ # Installs a package and starts it as root, in a unit of its own.
96
+ "proxy-switch-start",
97
+ "proxy-switch-run",
98
+ "proxy-switch-state",
99
+ "proxy-switch-recover",
93
100
  }
94
101
 
95
102
 
@@ -349,8 +356,8 @@ def restart_nodered(timeout: int = 120) -> Result:
349
356
  return run("restart-nodered", timeout=timeout)
350
357
 
351
358
 
352
- def pull_nodered(timeout: int = 600) -> Result:
353
- """Pull the Node-RED image."""
359
+ def pull_nodered(timeout: int = 960) -> Result:
360
+ """Pull the Node-RED image; outlasts the helper's own 900 s limit."""
354
361
  return run("pull-nodered", timeout=timeout)
355
362
 
356
363
 
@@ -453,6 +460,60 @@ def caddy_image_apply(timeout: int = 1300) -> Result:
453
460
  return run("caddy-image-apply", timeout=timeout)
454
461
 
455
462
 
463
+ #: Present once Caddy is the apt package under systemd. Created only by the
464
+ #: root helper's switch, and world-readable, so the application can look.
465
+ NATIVE_MARKER = Path("/etc/boneio/proxy-native")
466
+
467
+
468
+ #: Present while the cloud wildcard block is wanted by the packaged Caddy.
469
+ #: Created and removed by the root helper's cloud-template verbs, which is the
470
+ #: whole of what "the cloud template" means once there is no compose file.
471
+ CLOUD_MARKER = Path("/etc/boneio/proxy-cloud")
472
+
473
+
474
+ def proxy_mode() -> str:
475
+ """``"native"`` once Caddy runs as the package, else ``"container"``."""
476
+ try:
477
+ return "native" if NATIVE_MARKER.exists() else "container"
478
+ except OSError:
479
+ return "container"
480
+
481
+
482
+ def proxy_switch_start(timeout: int = 60) -> Result:
483
+ """Start the move from the Caddy container to the package, and return.
484
+
485
+ The helper runs it in a unit of its own, so it outlives a boneIO restart;
486
+ it refuses while one runs and once Caddy is already the package.
487
+ """
488
+ return run("proxy-switch-start", timeout=timeout)
489
+
490
+
491
+ def proxy_switch_state(timeout: int = 30) -> Result:
492
+ """The last switch's record and log tail, as JSON on stdout. Read-only."""
493
+ return run("proxy-switch-state", timeout=timeout)
494
+
495
+
496
+ def proxy_switch_recover(timeout: int = 300) -> Result:
497
+ """Undo a switch that was cut short; a no-op for anything else."""
498
+ return run("proxy-switch-recover", timeout=timeout)
499
+
500
+
501
+ def root_ca(timeout: int = 30) -> bytes | None:
502
+ """The PEM of Caddy's local root certificate, read by the helper.
503
+
504
+ One path for both modes: the helper knows where each keeps it, and in the
505
+ packaged one the data directory is not readable by the application.
506
+
507
+ Returns:
508
+ The certificate, or None when the helper is missing or too old to know
509
+ the verb, or Caddy has not created one yet.
510
+ """
511
+ result = run("caddy-root-ca", timeout=timeout)
512
+ if not result.ok or not result.stdout.strip():
513
+ return None
514
+ return result.stdout.encode()
515
+
516
+
456
517
  def remove_cloud_template(timeout: int = 60) -> Result:
457
518
  """Restore the plain compose template."""
458
519
  return run("remove-cloud-template", timeout=timeout)
@@ -539,6 +600,9 @@ def cloud_template_is_live() -> bool:
539
600
  cannot be read — the local template is the safe assumption, being the
540
601
  one that needs nothing from outside the device.
541
602
  """
603
+ if proxy_mode() == "native":
604
+ # No compose file to ask: the helper's marker is the template.
605
+ return CLOUD_MARKER.exists()
542
606
  try:
543
607
  body = COMPOSE_FILE.read_text()
544
608
  except OSError as err:
boneio/core/events/bus.py CHANGED
@@ -372,8 +372,14 @@ class EventBus:
372
372
  return listener_job
373
373
 
374
374
  def remove_event_listener(self, event_type: str | None = None, entity_id: str | None = None, listener_id: str | None = None) -> None:
375
- """Remove event listener. Can remove by event_type, listener_id, or both."""
376
- if listener_id and listener_id in self._listener_id_index:
375
+ """Remove event listener. Can remove by event_type, listener_id, or both.
376
+
377
+ A ``listener_id`` the bus does not know removes nothing; it never
378
+ widens to every listener of ``event_type``.
379
+ """
380
+ if listener_id:
381
+ if listener_id not in self._listener_id_index:
382
+ return
377
383
  # Remove by listener_id
378
384
  for evt_type, ent_id in self._listener_id_index[listener_id]:
379
385
  if event_type and evt_type != event_type:
@@ -830,7 +830,7 @@ class Manager:
830
830
 
831
831
  elif action == OUTPUT_OVER_MQTT:
832
832
  boneio_id = action_definition.get("boneio_id")
833
- action_output = action_definition.get("action_output")
833
+ action_output = action_definition.get("action_output", TOGGLE)
834
834
  action_to_execute = output_actions.get(action_output.upper())
835
835
  if boneio_id and action_to_execute:
836
836
  parsed_action = {
@@ -845,7 +845,7 @@ class Manager:
845
845
 
846
846
  elif action == COVER_OVER_MQTT:
847
847
  boneio_id = action_definition.get("boneio_id")
848
- action_cover = action_definition.get("action_cover")
848
+ action_cover = action_definition.get("action_cover", TOGGLE)
849
849
  action_to_execute = cover_actions.get(action_cover.upper())
850
850
  if boneio_id and action_to_execute:
851
851
  parsed_action = {
@@ -1494,11 +1494,12 @@ class Manager:
1494
1494
 
1495
1495
  _LOGGER.info("Reloading remote devices configuration")
1496
1496
  # Clean up old remote inputs and outputs before reload
1497
+ await self.inputs._remote_registrar.stop()
1497
1498
  self.inputs.unregister_remote_inputs()
1498
1499
  self.unregister_remote_outputs()
1499
1500
  await self.remote_devices.reload(remote_devices_config)
1500
1501
  # Re-register remote inputs and outputs from config
1501
- self.register_remote_inputs()
1502
+ await self.register_remote_inputs()
1502
1503
  self.register_remote_outputs()
1503
1504
  # Broadcast all input states so frontend picks up new/removed remote inputs
1504
1505
  self.inputs._broadcast_all_input_states()
@@ -1515,13 +1516,12 @@ class Manager:
1515
1516
  # previous callbacks would run the old actions alongside the new ones.
1516
1517
  await self.inputs._remote_registrar.stop()
1517
1518
  self.inputs.unregister_remote_inputs()
1518
- self.register_remote_inputs()
1519
- await self.inputs._remote_registrar.start()
1519
+ await self.register_remote_inputs()
1520
1520
  # Broadcast all input states so frontend picks up new/removed remote inputs
1521
1521
  self.inputs._broadcast_all_input_states()
1522
1522
  _LOGGER.info("Remote inputs configuration reloaded successfully")
1523
1523
 
1524
- def register_remote_inputs(self) -> None:
1524
+ async def register_remote_inputs(self) -> None:
1525
1525
  """Register remote inputs from the ``remote_inputs`` config section.
1526
1526
 
1527
1527
  Delegates to :meth:`InputManager.register_remote_inputs`.
@@ -1530,6 +1530,11 @@ class Manager:
1530
1530
  config = self._config_helper.get_config()
1531
1531
  remote_inputs_config = config.get("remote_inputs", [])
1532
1532
  self.inputs.register_remote_inputs(self.remote_devices, remote_inputs_config)
1533
+ # Subscribe here rather than wait for the next MQTT connect: at startup
1534
+ # the broker is usually connected long before remote inputs register,
1535
+ # and the inputs would stay deaf until the connection dropped. Before
1536
+ # the connect the bus only records the listener and subscribes it then.
1537
+ await self.inputs._remote_registrar.start()
1533
1538
 
1534
1539
  # Keep backward-compatible alias
1535
1540
  register_esphome_binary_sensors = register_remote_inputs
@@ -78,3 +78,9 @@ class MessageBus(ABC):
78
78
  """Unsubscribe from a topic and stop listening."""
79
79
  pass
80
80
 
81
+ async def wait_until_subscribed(self) -> None:
82
+ """Return once the broker holds this bus's subscriptions.
83
+
84
+ Until then a retained message cannot have arrived yet, so its absence
85
+ means nothing. Buses without a broker return at once.
86
+ """
@@ -97,3 +97,7 @@ class CompositeMessageBus(MessageBus):
97
97
  """Unsubscribe from a topic and stop listening on all buses."""
98
98
  for bus in self._buses:
99
99
  await bus.unsubscribe_and_stop_listen(topic)
100
+
101
+ async def wait_until_subscribed(self) -> None:
102
+ """Wait for every bus: one being up says nothing about another."""
103
+ await asyncio.gather(*(bus.wait_until_subscribed() for bus in self._buses))
@@ -76,6 +76,7 @@ class MQTTClient(MessageBus):
76
76
  self.asyncio_client = self.create_client()
77
77
  self.reconnect_interval = 1
78
78
  self._connection_established = False
79
+ self._subscribed = asyncio.Event()
79
80
  self.publish_queue: UniqueQueue = UniqueQueue()
80
81
  self._mqtt_energy_listeners: dict[str, Callable[..., Awaitable[None]]] = {}
81
82
  # Listeners that are told whether a message is a retained replay.
@@ -227,6 +228,10 @@ class MQTTClient(MessageBus):
227
228
  if self._connection_established:
228
229
  await self.subscribe(topics=[topic])
229
230
 
231
+ @override
232
+ async def wait_until_subscribed(self) -> None:
233
+ await self._subscribed.wait()
234
+
230
235
  @override
231
236
  async def unsubscribe_and_stop_listen(self, topic: str) -> None:
232
237
  await self.unsubscribe([topic])
@@ -476,6 +481,7 @@ class MQTTClient(MessageBus):
476
481
 
477
482
  topics = self._topics + list(self._mqtt_energy_listeners.keys()) + self._discovery_topics
478
483
  await self.subscribe(topics=topics)
484
+ self._subscribed.set()
479
485
  # Connected: the next outage starts the backoff from scratch
480
486
  # instead of waiting as long as the worst one before it.
481
487
  self.reconnect_interval = 1
@@ -0,0 +1,149 @@
1
+ """Moves a controller from the Caddy container to the packaged Caddy by itself.
2
+
3
+ The move is the root helper's (``proxy-switch-start``), run in a unit of its
4
+ own; this only decides when to ask for it and watches it finish. It waits for
5
+ the controller to have settled, needs the package repository to be reachable,
6
+ and gives up after a few failed runs so a controller that cannot make the move
7
+ does not retry for ever. Everything that touches the helper or the network runs
8
+ off the event loop, and nothing here waits on or blocks the inputs.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ import socket
16
+
17
+ from boneio.core import containers
18
+
19
+ _LOGGER = logging.getLogger(__name__)
20
+
21
+ #: Let the controller finish starting (discovery, Modbus, the first sensor
22
+ #: reads) before a package install competes with it for a single core.
23
+ START_DELAY = 300
24
+ #: Between looks when a condition was not met (offline, helper not updated).
25
+ RECHECK = 1800
26
+ POLL = 10
27
+ #: A switch that runs longer than this (30 minutes of polls) is left to the
28
+ #: next look rather than watched for ever.
29
+ MAX_POLLS = 180
30
+ MAX_ATTEMPTS = 3
31
+ #: Where the package comes from; no point starting the move without it.
32
+ REPO_HOST = "dl.cloudsmith.io"
33
+
34
+
35
+ def has_default_route() -> bool:
36
+ """Whether the kernel has a default IPv4 route (destination and mask zero)."""
37
+ try:
38
+ with open("/proc/net/route", encoding="ascii") as handle:
39
+ rows = [line.split() for line in handle.read().splitlines()[1:]]
40
+ except OSError:
41
+ return False
42
+ return any(len(r) > 7 and r[1] == "00000000" and r[7] == "00000000" for r in rows)
43
+
44
+
45
+ def repo_resolves() -> bool:
46
+ """Whether the package repository's name resolves. Blocks; call off the loop."""
47
+ try:
48
+ socket.getaddrinfo(REPO_HOST, 443)
49
+ except OSError:
50
+ return False
51
+ return True
52
+
53
+
54
+ def read_state() -> dict | None:
55
+ """The helper's switch record, or None when it cannot be read."""
56
+ result = containers.proxy_switch_state()
57
+ state = result.json() if result.ok else None
58
+ return state if isinstance(state, dict) else None
59
+
60
+
61
+ #: The ``at`` of the interrupted record already undone, so one record is not
62
+ #: undone twice; a later interrupted run has a new ``at``.
63
+ _recovered_at: object = object()
64
+
65
+
66
+ def _recover_if_cut_short(state: dict) -> dict | None:
67
+ """Undo a run the helper reports as interrupted, and read the record again.
68
+
69
+ The helper shows a ``running`` record with no unit as ``failed`` with the
70
+ error ``interrupted``; the record itself still says ``running`` until the
71
+ undo has run, which is what puts a stopped unit's container back. Covers
72
+ ``systemctl stop`` of the unit while boneIO keeps running (a boot unit
73
+ covers a reboot). Once per interrupted record.
74
+ """
75
+ global _recovered_at
76
+ if state.get("running") or state.get("error") != "interrupted":
77
+ return state
78
+ if state.get("at") == _recovered_at:
79
+ return state
80
+ _LOGGER.warning("Proxy switch was interrupted; undoing it")
81
+ _recovered_at = state.get("at")
82
+ containers.proxy_switch_recover()
83
+ return read_state()
84
+
85
+
86
+ def _finished(state: dict) -> bool:
87
+ """Whether no further automatic attempt is due."""
88
+ attempts = state.get("attempts")
89
+ return state.get("state") == "done" or (isinstance(attempts, int) and attempts >= MAX_ATTEMPTS)
90
+
91
+
92
+ async def attempt_switch() -> bool:
93
+ """One look: start the switch if every condition holds, and wait for it.
94
+
95
+ Returns:
96
+ Whether the automatic task is finished: Caddy is the package, or the
97
+ attempts are used up. False when it should look again later.
98
+ """
99
+ if not await asyncio.to_thread(containers.helper_supports, "proxy-switch-start"):
100
+ return False
101
+ state = await asyncio.to_thread(read_state)
102
+ if state is None:
103
+ return False
104
+ state = await asyncio.to_thread(_recover_if_cut_short, state)
105
+ if state is None:
106
+ return False
107
+ if await asyncio.to_thread(containers.proxy_mode) == "native":
108
+ return True
109
+ if state.get("running"):
110
+ return False
111
+ if _finished(state):
112
+ return True
113
+ if not await asyncio.to_thread(has_default_route):
114
+ return False
115
+ if not await asyncio.to_thread(repo_resolves):
116
+ return False
117
+
118
+ started = await asyncio.to_thread(containers.proxy_switch_start)
119
+ if not started.ok:
120
+ _LOGGER.warning("Proxy switch did not start: %s", (started.stderr or started.stdout).strip())
121
+ return False
122
+ _LOGGER.info("Proxy switch started")
123
+ for _ in range(MAX_POLLS):
124
+ await asyncio.sleep(POLL)
125
+ state = await asyncio.to_thread(read_state)
126
+ if state is not None and not state.get("running"):
127
+ break
128
+ if state is None:
129
+ return False
130
+ _LOGGER.info("Proxy switch finished: %s", state.get("state"))
131
+ # A unit stopped by hand ends here as interrupted; undo it now rather than
132
+ # at the next look, which may never come if the panel is what it broke.
133
+ state = await asyncio.to_thread(_recover_if_cut_short, state) or state
134
+ return _finished(state)
135
+
136
+
137
+ async def run() -> None:
138
+ """Background task: look 5 minutes after start, then every half hour.
139
+
140
+ Ends once Caddy is the package or the automatic attempts are used up.
141
+ """
142
+ await asyncio.sleep(START_DELAY)
143
+ while True:
144
+ try:
145
+ if await attempt_switch():
146
+ return
147
+ except Exception: # noqa: BLE001 - a failed look must not end the task
148
+ _LOGGER.exception("Proxy switch check failed")
149
+ await asyncio.sleep(RECHECK)
@@ -22,6 +22,8 @@ from pathlib import Path
22
22
  from cryptography import x509
23
23
  from cryptography.hazmat.primitives import hashes, serialization
24
24
 
25
+ from boneio.core import containers
26
+
25
27
  _LOGGER = logging.getLogger(__name__)
26
28
 
27
29
  #: Refuse anything larger. A certificate chain is a few kilobytes; a megabyte
@@ -231,6 +233,41 @@ ROOT_CA = (
231
233
  / "authorities" / "local" / "root.crt"
232
234
  )
233
235
 
236
+
237
+ def root_ca() -> bytes | None:
238
+ """Caddy's root certificate in whichever mode runs it.
239
+
240
+ Asked of the root helper, the only reader of the packaged Caddy's data. An
241
+ old helper that lacks the verb leaves the container mode reading the file
242
+ itself, as it always did; the packaged mode reads the generator's public
243
+ export, since its data directory is not the application's to read.
244
+
245
+ Returns:
246
+ The PEM, or None when there is none yet.
247
+ """
248
+ pem = containers.root_ca()
249
+ if pem is not None:
250
+ return pem
251
+ try:
252
+ return (
253
+ ROOT_CA_EXPORT if containers.proxy_mode() == "native" else ROOT_CA
254
+ ).read_bytes()
255
+ except OSError:
256
+ return None
257
+
258
+
259
+ #: The public copy of that certificate the packaged Caddy's generator writes
260
+ #: on every start and reload, world-readable, so that asking whether one exists
261
+ #: needs no sudo.
262
+ ROOT_CA_EXPORT = Path("/var/lib/boneio/proxy/root.crt")
263
+
264
+
265
+ def root_ca_exists() -> bool:
266
+ """Whether a root certificate is there to offer; a plain file check."""
267
+ path = ROOT_CA_EXPORT if containers.proxy_mode() == "native" else ROOT_CA
268
+ return path.exists()
269
+
270
+
234
271
  #: The key is a secret, and it is written by the account the panel runs as.
235
272
  KEY_MODE = 0o600
236
273
  CERT_MODE = 0o644
boneio/core/system_ops.py CHANGED
@@ -379,3 +379,13 @@ def os_update_log(timeout: int = 30) -> Result:
379
379
  def os_autoupdate_set(enabled: bool, timeout: int = 60) -> Result:
380
380
  """Switch automatic security updates on or off. Never reboots either way."""
381
381
  return run("os-autoupdate-set", "on" if enabled else "off", timeout=timeout)
382
+
383
+
384
+ def disk_usage(timeout: int = 120) -> Result:
385
+ """Docker images, the journal and the apt cache, in bytes, as JSON."""
386
+ return run("disk-usage", timeout=timeout)
387
+
388
+
389
+ def disk_clean(target: str, timeout: int = 360) -> Result:
390
+ """Free space in ``docker`` (unused images) or ``apt`` (the package cache)."""
391
+ return run("disk-clean", target, timeout=timeout)
@@ -25,11 +25,11 @@ binary_sensor: !include binary_sensor.yaml
25
25
  output: !include output24x16A.yaml
26
26
 
27
27
  boneio:
28
- config_version: 4
28
+ config_version: 7
29
29
  name: "boneIO Black 24x16A"
30
30
  version: 0.8
31
31
  device_type: "24x16A"
32
-
32
+
33
33
  modbus:
34
34
  uart: uart4
35
35
 
@@ -37,7 +37,12 @@ logger:
37
37
  default: info
38
38
 
39
39
  web:
40
- port: 8090 #If you change this you need to change Nginx proxy settings in /home/boneio/docker/
41
- proxy_port: 8443
42
- # adc: !include adc.yaml Uncomment only if needed. Not supported yet in v1.0.0
43
-
40
+ # Panel nie jest wystawiony po czystym HTTP na sieć lokalną.
41
+ #
42
+ # Wchodzisz na https://<adres_ip>:8443 (certyfikat self-signed, przeglądarka
43
+ # ostrzeże raz) albo po kablu USB na http://192.168.7.2:8090.
44
+ # Żeby wrócić do starego zachowania: expose: all
45
+ #
46
+ # Port nie jest tu powtórzony — domyślnie i tak jest to 8090.
47
+ expose: proxy
48
+ # adc: !include adc.yaml
@@ -1,6 +1,6 @@
1
1
  host: localhost
2
2
  username: boneio
3
- password: boneio123
3
+ password: !secret mqtt_password
4
4
  topic_prefix: boneio
5
5
  ha_discovery:
6
6
  enabled: yes
@@ -0,0 +1,4 @@
1
+ # Replaced at the controller's first boot by boneio-mqtt-firstboot with a
2
+ # password drawn for this device alone; the broker gets the same one. The
3
+ # factory value below only exists so the image's config validates.
4
+ mqtt_password: boneio123