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.
- boneio/components/sensor/virtual_energy.py +31 -19
- boneio/core/cloud/registration.py +15 -10
- boneio/core/config/provenance.py +7 -4
- boneio/core/containers.py +66 -2
- boneio/core/events/bus.py +8 -2
- boneio/core/manager/manager.py +11 -6
- boneio/core/messaging/basic.py +6 -0
- boneio/core/messaging/composite.py +4 -0
- boneio/core/messaging/mqtt.py +6 -0
- boneio/core/proxy_switch.py +149 -0
- boneio/core/security/certificate.py +37 -0
- boneio/core/system_ops.py +10 -0
- boneio/{example_config → factory_config/0.8}/24x16/config.yaml +11 -6
- boneio/{example_config → factory_config/0.8}/24x16/mqtt.yaml +1 -1
- boneio/factory_config/0.8/24x16/secrets.yaml +4 -0
- boneio/{example_config → factory_config/0.8}/32x10/config.yaml +10 -4
- boneio/{example_config → factory_config/0.8}/32x10/mqtt.yaml +1 -1
- boneio/factory_config/0.8/32x10/secrets.yaml +4 -0
- boneio/{example_config → factory_config/0.8}/48x4/config.yaml +10 -4
- boneio/{example_config → factory_config/0.8}/48x4/mqtt.yaml +1 -1
- boneio/factory_config/0.8/48x4/secrets.yaml +4 -0
- boneio/factory_config/0.8/base/config.yaml +42 -0
- boneio/{example_config/cover → factory_config/0.8/base}/mqtt.yaml +1 -1
- boneio/factory_config/0.8/base/secrets.yaml +4 -0
- boneio/{example_config → factory_config/0.8}/cover/config.yaml +11 -5
- boneio/factory_config/0.8/cover/mqtt.yaml +6 -0
- boneio/factory_config/0.8/cover/secrets.yaml +4 -0
- boneio/{example_config → factory_config/0.8}/cover_mix/config.yaml +11 -5
- boneio/factory_config/0.8/cover_mix/mqtt.yaml +6 -0
- boneio/factory_config/0.8/cover_mix/secrets.yaml +4 -0
- boneio/factory_config/1.0/24x16/adc.yaml +14 -0
- boneio/factory_config/1.0/24x16/binary_sensor.yaml +17 -0
- boneio/factory_config/1.0/24x16/config.yaml +48 -0
- boneio/factory_config/1.0/24x16/event.yaml +208 -0
- boneio/factory_config/1.0/24x16/mqtt.yaml +6 -0
- boneio/factory_config/1.0/24x16/output24x16A.yaml +96 -0
- boneio/factory_config/1.0/24x16/secrets.yaml +4 -0
- boneio/factory_config/1.0/32x10/adc.yaml +14 -0
- boneio/factory_config/1.0/32x10/binary_sensor.yaml +17 -0
- boneio/factory_config/1.0/32x10/config.yaml +48 -0
- boneio/factory_config/1.0/32x10/event.yaml +243 -0
- boneio/factory_config/1.0/32x10/mqtt.yaml +6 -0
- boneio/factory_config/1.0/32x10/output32x10A.yaml +128 -0
- boneio/factory_config/1.0/32x10/secrets.yaml +4 -0
- boneio/factory_config/1.0/48x4/adc.yaml +14 -0
- boneio/factory_config/1.0/48x4/binary_sensor.yaml +17 -0
- boneio/factory_config/1.0/48x4/config.yaml +48 -0
- boneio/factory_config/1.0/48x4/event.yaml +300 -0
- boneio/factory_config/1.0/48x4/mqtt.yaml +6 -0
- boneio/factory_config/1.0/48x4/output48x4A.yaml +192 -0
- boneio/factory_config/1.0/48x4/secrets.yaml +4 -0
- boneio/factory_config/1.0/base/config.yaml +42 -0
- boneio/factory_config/1.0/base/mqtt.yaml +6 -0
- boneio/factory_config/1.0/base/secrets.yaml +4 -0
- boneio/factory_config/1.0/cover/adc.yaml +14 -0
- boneio/factory_config/1.0/cover/binary_sensor.yaml +19 -0
- boneio/factory_config/1.0/cover/config.yaml +49 -0
- boneio/factory_config/1.0/cover/cover.yaml +112 -0
- boneio/factory_config/1.0/cover/event.yaml +244 -0
- boneio/factory_config/1.0/cover/mqtt.yaml +6 -0
- boneio/factory_config/1.0/cover/outputCover.yaml +65 -0
- boneio/factory_config/1.0/cover/secrets.yaml +4 -0
- boneio/factory_config/1.0/cover_mix/adc.yaml +14 -0
- boneio/factory_config/1.0/cover_mix/binary_sensor.yaml +19 -0
- boneio/factory_config/1.0/cover_mix/config.yaml +49 -0
- boneio/factory_config/1.0/cover_mix/cover.yaml +56 -0
- boneio/factory_config/1.0/cover_mix/event.yaml +242 -0
- boneio/factory_config/1.0/cover_mix/mqtt.yaml +6 -0
- boneio/factory_config/1.0/cover_mix/outputCoverMix.yaml +81 -0
- boneio/factory_config/1.0/cover_mix/secrets.yaml +4 -0
- boneio/factory_config/1.0/tester/adc.yaml +14 -0
- boneio/factory_config/1.0/tester/binary_sensor.yaml +17 -0
- boneio/factory_config/1.0/tester/config.yaml +49 -0
- boneio/factory_config/1.0/tester/event.yaml +243 -0
- boneio/factory_config/1.0/tester/mqtt.yaml +6 -0
- boneio/factory_config/1.0/tester/output32x10A.yaml +128 -0
- boneio/factory_config/1.0/tester/secrets.yaml +4 -0
- boneio/factory_config/1.1/24x16/adc.yaml +14 -0
- boneio/factory_config/1.1/24x16/binary_sensor.yaml +17 -0
- boneio/factory_config/1.1/24x16/config.yaml +48 -0
- boneio/factory_config/1.1/24x16/event.yaml +208 -0
- boneio/factory_config/1.1/24x16/mqtt.yaml +6 -0
- boneio/factory_config/1.1/24x16/output24x16A.yaml +96 -0
- boneio/factory_config/1.1/24x16/secrets.yaml +4 -0
- boneio/factory_config/1.1/32x10/adc.yaml +14 -0
- boneio/factory_config/1.1/32x10/binary_sensor.yaml +17 -0
- boneio/factory_config/1.1/32x10/config.yaml +48 -0
- boneio/factory_config/1.1/32x10/event.yaml +243 -0
- boneio/factory_config/1.1/32x10/mqtt.yaml +6 -0
- boneio/factory_config/1.1/32x10/output32x10A.yaml +128 -0
- boneio/factory_config/1.1/32x10/secrets.yaml +4 -0
- boneio/factory_config/1.1/48x4/adc.yaml +14 -0
- boneio/factory_config/1.1/48x4/binary_sensor.yaml +17 -0
- boneio/factory_config/1.1/48x4/config.yaml +48 -0
- boneio/factory_config/1.1/48x4/event.yaml +300 -0
- boneio/factory_config/1.1/48x4/mqtt.yaml +6 -0
- boneio/factory_config/1.1/48x4/output48x4A.yaml +192 -0
- boneio/factory_config/1.1/48x4/secrets.yaml +4 -0
- boneio/factory_config/1.1/base/config.yaml +42 -0
- boneio/factory_config/1.1/base/mqtt.yaml +6 -0
- boneio/factory_config/1.1/base/secrets.yaml +4 -0
- boneio/factory_config/1.1/cover/adc.yaml +14 -0
- boneio/factory_config/1.1/cover/binary_sensor.yaml +19 -0
- boneio/factory_config/1.1/cover/config.yaml +49 -0
- boneio/factory_config/1.1/cover/cover.yaml +112 -0
- boneio/factory_config/1.1/cover/event.yaml +244 -0
- boneio/factory_config/1.1/cover/mqtt.yaml +6 -0
- boneio/factory_config/1.1/cover/outputCover.yaml +65 -0
- boneio/factory_config/1.1/cover/secrets.yaml +4 -0
- boneio/factory_config/1.1/cover_mix/adc.yaml +14 -0
- boneio/factory_config/1.1/cover_mix/binary_sensor.yaml +19 -0
- boneio/factory_config/1.1/cover_mix/config.yaml +49 -0
- boneio/factory_config/1.1/cover_mix/cover.yaml +56 -0
- boneio/factory_config/1.1/cover_mix/event.yaml +242 -0
- boneio/factory_config/1.1/cover_mix/mqtt.yaml +6 -0
- boneio/factory_config/1.1/cover_mix/outputCoverMix.yaml +81 -0
- boneio/factory_config/1.1/cover_mix/secrets.yaml +4 -0
- boneio/factory_config/1.1/tester/adc.yaml +14 -0
- boneio/factory_config/1.1/tester/binary_sensor.yaml +17 -0
- boneio/factory_config/1.1/tester/config.yaml +49 -0
- boneio/factory_config/1.1/tester/event.yaml +243 -0
- boneio/factory_config/1.1/tester/mqtt.yaml +6 -0
- boneio/factory_config/1.1/tester/output32x10A.yaml +128 -0
- boneio/factory_config/1.1/tester/secrets.yaml +4 -0
- boneio/factory_config/__init__.py +41 -0
- boneio/migrations/assets/MANIFEST.sha256 +13 -4
- boneio/migrations/assets/apt/53boneio-caddy +17 -0
- boneio/migrations/assets/apt/caddy-pin.pref +3 -0
- boneio/migrations/assets/apt/caddy-stable-archive-keyring.gpg +0 -0
- boneio/migrations/assets/apt/caddy-stable.list +1 -0
- boneio/migrations/assets/caddy/502.html +35 -0
- boneio/migrations/assets/docker/nodered/caddy/502.html +22 -30
- boneio/migrations/assets/docker/nodered/docker-compose-native-proxy.yaml +23 -0
- boneio/migrations/assets/helpers/boneio-containers +1134 -38
- boneio/migrations/assets/helpers/boneio-helpers-heal +17 -2
- boneio/migrations/assets/helpers/boneio-proxy-config +677 -0
- boneio/migrations/assets/helpers/boneio-system +118 -1
- boneio/migrations/assets/systemd/boneio-proxy-recover.service +24 -0
- boneio/migrations/assets/systemd/caddy-boneio.conf +44 -0
- boneio/migrations/plans/1.4.4.json +1 -1
- boneio/migrations/plans/1.4.4.sig +0 -0
- boneio/migrations/plans/1.6.17.json +1 -1
- boneio/migrations/plans/1.6.17.sig +1 -1
- boneio/migrations/plans/1.6.18.json +1 -1
- boneio/migrations/plans/1.6.18.sig +1 -1
- boneio/migrations/plans/1.6.21.json +1 -1
- boneio/migrations/plans/1.6.21.sig +1 -1
- boneio/migrations/plans/1.6.22.json +1 -1
- boneio/migrations/plans/1.6.22.sig +1 -3
- boneio/migrations/plans/1.6.26.json +1 -1
- boneio/migrations/plans/1.6.26.sig +1 -1
- boneio/migrations/plans/1.6.27.json +1 -1
- boneio/migrations/plans/1.6.27.sig +1 -1
- boneio/migrations/plans/1.6.29.json +1 -1
- boneio/migrations/plans/1.6.29.sig +1 -1
- boneio/migrations/plans/1.6.30.json +1 -1
- boneio/migrations/plans/1.6.30.sig +1 -1
- boneio/migrations/plans/1.6.31.json +1 -1
- boneio/migrations/plans/1.6.31.sig +1 -1
- boneio/migrations/plans/1.6.32.json +1 -1
- boneio/migrations/plans/1.6.32.sig +2 -1
- boneio/migrations/plans/1.6.33.json +1 -1
- boneio/migrations/plans/1.6.33.sig +1 -1
- boneio/migrations/plans/1.6.39.json +1 -0
- boneio/migrations/plans/1.6.39.sig +1 -0
- boneio/migrations/plans/1.6.40.json +1 -0
- boneio/migrations/plans/1.6.40.sig +1 -0
- boneio/migrations/plans/1.6.41.json +1 -0
- boneio/migrations/plans/1.6.41.sig +2 -0
- boneio/migrations/plans/1.6.42.json +1 -0
- boneio/migrations/plans/1.6.42.sig +1 -0
- boneio/migrations/plans/1.6.43.json +1 -0
- boneio/migrations/plans/1.6.43.sig +0 -0
- boneio/migrations/plans/1.6.5.json +1 -1
- boneio/migrations/plans/1.6.5.sig +1 -2
- boneio/migrations/plans/1.6.8.json +1 -1
- boneio/migrations/plans/1.6.8.sig +0 -0
- boneio/migrations/plans/1.6.9.json +1 -1
- boneio/migrations/plans/1.6.9.sig +1 -1
- boneio/migrations/plans/manifest.json +1 -1
- boneio/migrations/plans/manifest.sig +1 -1
- boneio/migrations/versions/v1_6_39_containers_pull_timeout.py +41 -0
- boneio/migrations/versions/v1_6_40_disk_usage.py +43 -0
- boneio/migrations/versions/v1_6_41_starting_page.py +29 -0
- boneio/migrations/versions/v1_6_42_native_caddy_staging.py +140 -0
- boneio/migrations/versions/v1_6_43_log2ram_cp.py +34 -0
- boneio/runner.py +8 -1
- boneio/schema/schema.yaml +4 -1
- boneio/version.py +1 -1
- boneio/webui/app.py +3 -0
- boneio/webui/bind.py +63 -3
- boneio/webui/frontend-dist/assets/{AccountsView-Cbqd4Ao_.js → AccountsView-AMfOhw42.js} +1 -1
- boneio/webui/frontend-dist/assets/AccountsView-AMfOhw42.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{AppInitContext-DWvK3qS_.js → AppInitContext-BDF1KqeY.js} +1 -1
- boneio/webui/frontend-dist/assets/AppInitContext-BDF1KqeY.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{BackupSection-8QSuOR0c.js → BackupSection-D7XzmmGa.js} +1 -1
- boneio/webui/frontend-dist/assets/BackupSection-D7XzmmGa.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{BindingMatrix-CfzTlbKI.js → BindingMatrix-Dm7enV81.js} +1 -1
- boneio/webui/frontend-dist/assets/BindingMatrix-Dm7enV81.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{DeviceControlSection-BDnXyBJT.js → DeviceControlSection-CbHya4k0.js} +1 -1
- boneio/webui/frontend-dist/assets/DeviceControlSection-CbHya4k0.js.gz +0 -0
- boneio/webui/frontend-dist/assets/DiagnosticsView-BiF5br1t.js +5 -0
- boneio/webui/frontend-dist/assets/DiagnosticsView-BiF5br1t.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{EntityGrid-DaGv-7ct.js → EntityGrid-4a1Dbqth.js} +1 -1
- boneio/webui/frontend-dist/assets/EntityGrid-4a1Dbqth.js.gz +0 -0
- boneio/webui/frontend-dist/assets/FactoryResetSection-Dsi68Z8u.js +1 -0
- boneio/webui/frontend-dist/assets/FactoryResetSection-Dsi68Z8u.js.gz +0 -0
- boneio/webui/frontend-dist/assets/FormInputToggle-Buzoogqy.js +11 -0
- boneio/webui/frontend-dist/assets/FormInputToggle-Buzoogqy.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{HaDashboardWizard-B4KWLmIy.js → HaDashboardWizard-AD5uYoaQ.js} +1 -1
- boneio/webui/frontend-dist/assets/HaDashboardWizard-AD5uYoaQ.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{HostnameSection-IfbaLobN.js → HostnameSection-tiU6n56p.js} +1 -1
- boneio/webui/frontend-dist/assets/HostnameSection-tiU6n56p.js.gz +0 -0
- boneio/webui/frontend-dist/assets/InputsView-D4MA7EqS.js +1 -0
- boneio/webui/frontend-dist/assets/InputsView-D4MA7EqS.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{LoadingState-Bp0pGOdE.js → LoadingState-CxmjkMgn.js} +1 -1
- boneio/webui/frontend-dist/assets/{LocationSection-CEDOjibw.js → LocationSection-BgvLiPAD.js} +2 -2
- boneio/webui/frontend-dist/assets/LocationSection-BgvLiPAD.js.gz +0 -0
- boneio/webui/frontend-dist/assets/LoggerForm-Be95erZp.js +1 -0
- boneio/webui/frontend-dist/assets/LoggerForm-Be95erZp.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{LongPressWrapper-B9Hl3ltV.js → LongPressWrapper-C266DAt0.js} +1 -1
- boneio/webui/frontend-dist/assets/LongPressWrapper-C266DAt0.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{MigrationsSection-D3xuvZyO.js → MigrationsSection-Dg3dgGBw.js} +1 -1
- boneio/webui/frontend-dist/assets/MigrationsSection-Dg3dgGBw.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{ModbusView-Bg5HDFqc.js → ModbusView-g7kLvU1N.js} +1 -1
- boneio/webui/frontend-dist/assets/ModbusView-g7kLvU1N.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{MqttPasswordsSection-CMVKRHVp.js → MqttPasswordsSection-9qBygV7e.js} +1 -1
- boneio/webui/frontend-dist/assets/MqttPasswordsSection-9qBygV7e.js.gz +0 -0
- boneio/webui/frontend-dist/assets/NodeRedManagement-CaMi1mNV.js +1 -0
- boneio/webui/frontend-dist/assets/NodeRedManagement-CaMi1mNV.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{NodeRedView-CP86NnC4.js → NodeRedView-BZdmAtAt.js} +1 -1
- boneio/webui/frontend-dist/assets/NodeRedView-BZdmAtAt.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{NumericInput-CrVNlFps.js → NumericInput-fo5QZMhp.js} +1 -1
- boneio/webui/frontend-dist/assets/NumericInput-fo5QZMhp.js.gz +0 -0
- boneio/webui/frontend-dist/assets/OutputsView-C-HNqu4_.js +1 -0
- boneio/webui/frontend-dist/assets/OutputsView-C-HNqu4_.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{SecurityView-C8VMMZ2Q.js → SecurityView-Bj3K9I1b.js} +1 -1
- boneio/webui/frontend-dist/assets/SecurityView-Bj3K9I1b.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{SensorView-DMh2GK_I.js → SensorView-CgO65fQs.js} +1 -1
- boneio/webui/frontend-dist/assets/SensorView-CgO65fQs.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{Sparkline-6iDv_8hd.js → Sparkline-DoHMYvvZ.js} +2 -2
- boneio/webui/frontend-dist/assets/Sparkline-DoHMYvvZ.js.gz +0 -0
- boneio/webui/frontend-dist/assets/SystemState-Cm6R3WLd.js +2 -0
- boneio/webui/frontend-dist/assets/SystemState-Cm6R3WLd.js.gz +0 -0
- boneio/webui/frontend-dist/assets/TemplatesView-DZVBJt5J.js +1 -0
- boneio/webui/frontend-dist/assets/TemplatesView-DZVBJt5J.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{TilePicker-DZrA7qwM.js → TilePicker-BkLaQi0H.js} +1 -1
- boneio/webui/frontend-dist/assets/TilePicker-BkLaQi0H.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{TimezoneSection-CA36pxfJ.js → TimezoneSection-B0y1rM9Q.js} +1 -1
- boneio/webui/frontend-dist/assets/TimezoneSection-B0y1rM9Q.js.gz +0 -0
- boneio/webui/frontend-dist/assets/UISettings-DihZLsQT.js +102 -0
- boneio/webui/frontend-dist/assets/UISettings-DihZLsQT.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{UpdateSafetyNotice-CAKxJzbp.js → UpdateSafetyNotice-Mp0jJZ1F.js} +1 -1
- boneio/webui/frontend-dist/assets/UpdateSafetyNotice-Mp0jJZ1F.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{ViewToggle-CVWoIKqI.js → ViewToggle-CUtK4zn3.js} +1 -1
- boneio/webui/frontend-dist/assets/{aiConfig-BB8qvNZ5.js → aiConfig-C5JZrzmG.js} +1 -1
- boneio/webui/frontend-dist/assets/aiConfig-C5JZrzmG.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{aiWizardPrompt-HWioCYHg.js → aiWizardPrompt-BW96nE6h.js} +1 -1
- boneio/webui/frontend-dist/assets/aiWizardPrompt-BW96nE6h.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{dialog-DMaCttcv.js → dialog-DcnAJg2-.js} +1 -1
- boneio/webui/frontend-dist/assets/dialog-DcnAJg2-.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{fa-ChDOCx3T.js → fa-wf69YpA7.js} +1 -1
- boneio/webui/frontend-dist/assets/fa-wf69YpA7.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{gi-DiFRP1nv.js → gi-CdbYopzz.js} +1 -1
- boneio/webui/frontend-dist/assets/{groupByArea-DIIjg3E_.js → groupByArea-CGu58rU4.js} +1 -1
- boneio/webui/frontend-dist/assets/groupByArea-CGu58rU4.js.gz +0 -0
- boneio/webui/frontend-dist/assets/index-BiDbe4jH.css +2 -0
- boneio/webui/frontend-dist/assets/{index-ybynnlsQ.css.gz → index-BiDbe4jH.css.gz} +0 -0
- boneio/webui/frontend-dist/assets/index-CO67NAHS.js +46 -0
- boneio/webui/frontend-dist/assets/index-CO67NAHS.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{monaco-CoUZsnkn.js → monaco-BNnzf817.js} +1 -1
- boneio/webui/frontend-dist/assets/monaco-BNnzf817.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{monaco-BksocEXi.js → monaco-CAkAVZil.js} +3 -3
- boneio/webui/frontend-dist/assets/monaco-CAkAVZil.js.gz +0 -0
- boneio/webui/frontend-dist/assets/{select-iQAUlBVk.js → select-DsYydCvY.js} +1 -1
- boneio/webui/frontend-dist/assets/select-DsYydCvY.js.gz +0 -0
- boneio/webui/frontend-dist/index.html +5 -5
- boneio/webui/frontend-dist/schema/config.schema.json +1 -1
- boneio/webui/frontend-dist/schema/web.schema.json +1 -1
- boneio/webui/frontend-dist/sw.js +1 -1
- boneio/webui/middleware/policy.py +7 -0
- boneio/webui/routes/__init__.py +2 -0
- boneio/webui/routes/config_actions.py +6 -4
- boneio/webui/routes/config_core.py +75 -0
- boneio/webui/routes/diagnostics.py +55 -0
- boneio/webui/routes/migrations.py +3 -1
- boneio/webui/routes/nodered.py +45 -5
- boneio/webui/routes/onboarding.py +126 -1
- boneio/webui/routes/os_update.py +5 -0
- boneio/webui/routes/proxy.py +62 -0
- boneio/webui/routes/security.py +28 -11
- boneio/webui/routes/system.py +3 -1
- boneio/webui/routes/update.py +52 -21
- boneio/webui/schema/config.schema.json +1 -1
- boneio/webui/schema/web.schema.json +1 -1
- boneio/webui/web_server.py +88 -2
- boneio-1.6.0.dev35.dist-info/METADATA +109 -0
- {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/RECORD +324 -196
- boneio/example_config/__init__.py +0 -0
- boneio/example_config/cover_mix/mqtt.yaml +0 -6
- boneio/webui/frontend-dist/assets/AccountsView-Cbqd4Ao_.js.gz +0 -0
- boneio/webui/frontend-dist/assets/AppInitContext-DWvK3qS_.js.gz +0 -0
- boneio/webui/frontend-dist/assets/BackupSection-8QSuOR0c.js.gz +0 -0
- boneio/webui/frontend-dist/assets/BindingMatrix-CfzTlbKI.js.gz +0 -0
- boneio/webui/frontend-dist/assets/DeviceControlSection-BDnXyBJT.js.gz +0 -0
- boneio/webui/frontend-dist/assets/DiagnosticsView-BFbE2gzt.js +0 -5
- boneio/webui/frontend-dist/assets/DiagnosticsView-BFbE2gzt.js.gz +0 -0
- boneio/webui/frontend-dist/assets/EntityGrid-DaGv-7ct.js.gz +0 -0
- boneio/webui/frontend-dist/assets/FactoryResetSection-C-k28afU.js +0 -1
- boneio/webui/frontend-dist/assets/FactoryResetSection-C-k28afU.js.gz +0 -0
- boneio/webui/frontend-dist/assets/FormInputToggle-BgXGSDAE.js +0 -11
- boneio/webui/frontend-dist/assets/FormInputToggle-BgXGSDAE.js.gz +0 -0
- boneio/webui/frontend-dist/assets/HaDashboardWizard-B4KWLmIy.js.gz +0 -0
- boneio/webui/frontend-dist/assets/HostnameSection-IfbaLobN.js.gz +0 -0
- boneio/webui/frontend-dist/assets/InputsView-LHCSi3jQ.js +0 -1
- boneio/webui/frontend-dist/assets/InputsView-LHCSi3jQ.js.gz +0 -0
- boneio/webui/frontend-dist/assets/LocationSection-CEDOjibw.js.gz +0 -0
- boneio/webui/frontend-dist/assets/LoggerForm-C2G_nFBk.js +0 -1
- boneio/webui/frontend-dist/assets/LoggerForm-C2G_nFBk.js.gz +0 -0
- boneio/webui/frontend-dist/assets/LongPressWrapper-B9Hl3ltV.js.gz +0 -0
- boneio/webui/frontend-dist/assets/MigrationsSection-D3xuvZyO.js.gz +0 -0
- boneio/webui/frontend-dist/assets/ModbusView-Bg5HDFqc.js.gz +0 -0
- boneio/webui/frontend-dist/assets/MqttPasswordsSection-CMVKRHVp.js.gz +0 -0
- boneio/webui/frontend-dist/assets/NodeRedManagement-8LQGoZFd.js +0 -1
- boneio/webui/frontend-dist/assets/NodeRedManagement-8LQGoZFd.js.gz +0 -0
- boneio/webui/frontend-dist/assets/NodeRedView-CP86NnC4.js.gz +0 -0
- boneio/webui/frontend-dist/assets/NumericInput-CrVNlFps.js.gz +0 -0
- boneio/webui/frontend-dist/assets/OutputsView-CkgUuZDI.js +0 -1
- boneio/webui/frontend-dist/assets/OutputsView-CkgUuZDI.js.gz +0 -0
- boneio/webui/frontend-dist/assets/SecurityView-C8VMMZ2Q.js.gz +0 -0
- boneio/webui/frontend-dist/assets/SensorView-DMh2GK_I.js.gz +0 -0
- boneio/webui/frontend-dist/assets/Sparkline-6iDv_8hd.js.gz +0 -0
- boneio/webui/frontend-dist/assets/SystemState-DUOSa8P5.js +0 -2
- boneio/webui/frontend-dist/assets/SystemState-DUOSa8P5.js.gz +0 -0
- boneio/webui/frontend-dist/assets/TemplatesView-DVP3Ifd4.js +0 -1
- boneio/webui/frontend-dist/assets/TemplatesView-DVP3Ifd4.js.gz +0 -0
- boneio/webui/frontend-dist/assets/TilePicker-DZrA7qwM.js.gz +0 -0
- boneio/webui/frontend-dist/assets/TimezoneSection-CA36pxfJ.js.gz +0 -0
- boneio/webui/frontend-dist/assets/UISettings-CQ64K21V.js +0 -102
- boneio/webui/frontend-dist/assets/UISettings-CQ64K21V.js.gz +0 -0
- boneio/webui/frontend-dist/assets/UpdateSafetyNotice-CAKxJzbp.js.gz +0 -0
- boneio/webui/frontend-dist/assets/aiConfig-BB8qvNZ5.js.gz +0 -0
- boneio/webui/frontend-dist/assets/aiWizardPrompt-HWioCYHg.js.gz +0 -0
- boneio/webui/frontend-dist/assets/dialog-DMaCttcv.js.gz +0 -0
- boneio/webui/frontend-dist/assets/fa-ChDOCx3T.js.gz +0 -0
- boneio/webui/frontend-dist/assets/groupByArea-DIIjg3E_.js.gz +0 -0
- boneio/webui/frontend-dist/assets/index-Co3agMvu.js +0 -42
- boneio/webui/frontend-dist/assets/index-Co3agMvu.js.gz +0 -0
- boneio/webui/frontend-dist/assets/index-ybynnlsQ.css +0 -2
- boneio/webui/frontend-dist/assets/monaco-BksocEXi.js.gz +0 -0
- boneio/webui/frontend-dist/assets/monaco-CoUZsnkn.js.gz +0 -0
- boneio/webui/frontend-dist/assets/select-iQAUlBVk.js.gz +0 -0
- boneio-1.6.0.dev33.dist-info/METADATA +0 -69
- /boneio/{example_config → factory_config/0.8}/24x16/adc.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/24x16/binary_sensor.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/24x16/event.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/24x16/output24x16A.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/32x10/adc.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/32x10/binary_sensor.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/32x10/event.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/32x10/output32x10A.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/48x4/adc.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/48x4/binary_sensor.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/48x4/event.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/48x4/output48x4A.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover/adc.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover/binary_sensor.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover/cover.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover/event.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover/outputCover.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover_mix/adc.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover_mix/binary_sensor.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover_mix/cover.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover_mix/event.yaml +0 -0
- /boneio/{example_config → factory_config/0.8}/cover_mix/outputCoverMix.yaml +0 -0
- {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/WHEEL +0 -0
- {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/entry_points.txt +0 -0
- {boneio-1.6.0.dev33.dist-info → boneio-1.6.0.dev35.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
#
|
|
173
|
-
#
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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:
|
boneio/core/config/provenance.py
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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:
|
boneio/core/manager/manager.py
CHANGED
|
@@ -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
|
boneio/core/messaging/basic.py
CHANGED
|
@@ -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))
|
boneio/core/messaging/mqtt.py
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
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
|