diff --git a/CHANGELOG.md b/CHANGELOG.md index 8480387..628f22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ## [Unreleased] ### Added +- **Every manifest now states where its driver talks, and what a manufacturer demands before it will talk at all** — `connectivity: local | cloud` plus an optional `setup:` list. `protocol` answered neither question: `nibe_local` and `myuplink` are both `protocol: http` and both read a NIBE heat pump, and they are opposites — one reads the pump over the LAN, the other reads NIBE's cloud. Backfilled across all 80 drivers from what the sources do: **77 local, 3 cloud** (`myuplink`, `easee_cloud`, `tibber`, the only drivers that hardcode a vendor endpoint — every other HTTP driver builds its base URL from config, and all 51 Modbus, 8 MQTT and 4 serial drivers name a vendor only as `homepage`) +- `setup` is the second axis and the more interesting one, because a driver can be `local` and still locked until somebody unlocks the interface. `device_screen` (nibe_local, enabled in the pump's own installer menu — no app, no account, confirmed on an S735), `device_ui` (sonnen's JSON API and token, CTEK's `ModbusTCPEnable`), `installer` (sma_pv's "one-time installer step in the WebUI / Sunny Portal"), `vendor_portal` (the three cloud drivers), `bridge` (heishamon, opendtu ×2, esphome_dsmr ×2, tesla_vehicle's TeslaBleHttpProxy, zuidwijk_p1, and goodwe's required dongle). 17 drivers carry a recorded gate; 63 do not +- **An absent `setup` means unrecorded, and the page says exactly that.** It renders "Not recorded", never "Nothing to set up", and a driver with none drops out of the setup filter rather than passing as though it needed nothing. Only `setup: [none]` is a claim, and nothing claims it yet. The same reason `verification_status` is never flattened into a "verified" tick: the catalog reports what is known, not what would look complete +- `tests/test_connectivity.py` keeps `connectivity` checkable rather than decorative. A cloud endpoint has to be hardcoded — there is nowhere else for it to come from — while a LAN address arrives from config, so the test reads that asymmetry: a driver claiming `local` must build no request against a public hostname, and one claiming `cloud` must name the service it needs. On the current catalog it separates exactly the three cloud drivers from the other 77. It also requires a cloud driver to record what its vendor demands, so nobody meets the account requirement only after installing +- The catalog page gains a **Reach** column and two filter groups, putting "works without the internet" and "needs a bridge device" one click away. Neither field reaches the signed artifact — `_load_channel` copies an explicit allowlist and these are not in it — so unlike `ders`, correcting one costs no driver version - **`upstream_docs`** — an optional manifest field (`url`, `title`, `kind`, `url_stability`) naming the vendor documents a driver was built against — a register map, a parameter changelog — at a semi-persistent URL. A driver decodes a device by following that document; when it moves upstream, the driver can fall behind and nobody notices for months. Recording the URL lets a watcher poll it and flag the driver for review. `url_stability` (`committed`/`stable`/`volatile`/`unknown`) records whether the manufacturer keeps the URL put, so a broken `stable` link reads as a real signal and a broken `volatile` one as routine. `nibe_local` and `myuplink` are the first to declare it, both pointing at NIBE's myUplink register changelog. `tools/validate_manifest.py` holds each entry to an `http(s)` URL and known `kind`/`url_stability` values; the field is descriptive metadata only and never reaches `index.yaml` or how a driver installs. Specified in `spec/manifest-v2.md` (V2.3), and `docs/WRITING-A-DRIVER.md` and `AGENTS.md` now tell a driver author to record it beside the older instruction to write down what they learned about the registers — where the knowledge came from is the half that was never asked for, and a field nobody is told to populate stays at two manifests out of 80. Both say what it cannot do: a document behind a login is not fetchable and belongs in a driver comment instead. Tested in `tests/test_upstream_docs.py` - **`watch-upstream-docs`** — the automation half of `upstream_docs`. `tools/check_upstream_docs.py` fetches every watched URL, hashes it, and diffs it against a committed baseline (`upstream-docs-state.json`); the weekly `.github/workflows/watch-upstream-docs.yml` opens a tracking issue when a document changes (registers to review) or goes missing (link rotted), weighted by `url_stability`. It notifies **once** per event — a change updates the baseline so it can't re-fire, an outage alerts only on crossing a three-run failure threshold, and the workflow skips a title that already has an open issue. Detection is a raw-byte hash, so a flagged change is a prompt to look, not proof the registers moved — the issue says so. Tested in `tests/test_upstream_docs_watch.py` - **The catalog page shows the vendor documents a driver was decoded from.** `upstream_docs` reached the manifests and the weekly watcher but never the page, so the one artefact most readers actually open still named 80 drivers without saying where any of them came from — and the field's whole purpose is to make that source findable again. Each declared document is now a link in the driver's card, under its title, with the kind (`Changelog`, `Register map`, …) and what `url_stability` claims. The stability wording is spelled out rather than printed as the bare enum: "stable" beside a link reads as a promise that the *registers* are settled, which is the opposite of why the field exists, so the page says "URL stable in practice". A short note explains that a flagged change is a prompt to review, not evidence the driver is wrong. Document titles join the search index, so the changelog a driver follows is findable by name. The generator re-checks the `http(s)` scheme it renders — `validate_manifest.py` holds the same line, but it is a different tool run at a different time, and this is the one place where the value becomes an `href` somebody clicks. `test_the_card_renders_the_documents_it_collects` guards the rendering rather than the payload, because collecting the documents and silently dropping them from the card is exactly the state that made this necessary diff --git a/index.yaml b/index.yaml index 17978c4..45aefd1 100644 --- a/index.yaml +++ b/index.yaml @@ -4,6 +4,7 @@ drivers: version: "1.0.6" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 3865 @@ -12,6 +13,7 @@ drivers: version: "1.0.3" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 3779 @@ -20,6 +22,7 @@ drivers: version: "1.0.4" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 3412 @@ -28,6 +31,7 @@ drivers: version: "1.0.4" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 4649 @@ -36,6 +40,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 4031 @@ -44,6 +49,7 @@ drivers: version: "1.2.0" tier: core protocol: mqtt + connectivity: local ders: [battery, meter, v2x_charger] control: true size_bytes: 7203 @@ -52,6 +58,7 @@ drivers: version: "1.1.0" tier: core protocol: mqtt + connectivity: local ders: [v2x_charger] control: true size_bytes: 7506 @@ -60,6 +67,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 6790 @@ -68,6 +76,7 @@ drivers: version: "1.0.5" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 4491 @@ -76,6 +85,7 @@ drivers: version: "1.0.5" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 4047 @@ -84,6 +94,7 @@ drivers: version: "1.0.5" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 4749 @@ -92,6 +103,8 @@ drivers: version: "0.3.1" tier: core protocol: modbus + connectivity: local + setup: [device_ui] ders: [ev] control: true size_bytes: 14921 @@ -100,6 +113,8 @@ drivers: version: "0.3.1" tier: core protocol: modbus + connectivity: local + setup: [device_ui] ders: [ev] control: true size_bytes: 27291 @@ -108,6 +123,8 @@ drivers: version: "0.3.1" tier: core protocol: modbus + connectivity: local + setup: [device_ui] ders: [ev] control: true size_bytes: 12072 @@ -116,6 +133,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [battery, meter, pv] control: false size_bytes: 23467 @@ -124,6 +142,7 @@ drivers: version: "1.0.4" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 4054 @@ -132,6 +151,8 @@ drivers: version: "1.1.0" tier: core protocol: http + connectivity: cloud + setup: [vendor_portal] ders: [ev] control: true size_bytes: 31312 @@ -140,6 +161,8 @@ drivers: version: "1.0.3" tier: community protocol: http + connectivity: local + setup: [bridge] ders: [meter] control: false size_bytes: 16772 @@ -148,6 +171,8 @@ drivers: version: "1.0.3" tier: core protocol: http + connectivity: local + setup: [bridge] ders: [meter] control: true size_bytes: 19353 @@ -156,6 +181,7 @@ drivers: version: "1.0.3" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 3545 @@ -164,6 +190,7 @@ drivers: version: "2.1.0" tier: core protocol: mqtt + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 55058 @@ -172,6 +199,7 @@ drivers: version: "2.1.0" tier: core protocol: mqtt + connectivity: local ders: [v2x_charger] control: true size_bytes: 13321 @@ -180,6 +208,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 14450 @@ -188,6 +217,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 6933 @@ -196,6 +226,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [battery, meter, pv] control: false size_bytes: 12721 @@ -204,6 +235,7 @@ drivers: version: "1.0.2" tier: community protocol: http + connectivity: local ders: [pv, meter] control: false size_bytes: 2080 @@ -212,6 +244,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 7521 @@ -220,6 +253,7 @@ drivers: version: "1.0.3" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 3700 @@ -228,6 +262,7 @@ drivers: version: "1.0.1" tier: community protocol: http + connectivity: local ders: [v2x_charger] control: false size_bytes: 3340 @@ -236,6 +271,8 @@ drivers: version: "2.1.1" tier: community protocol: modbus + connectivity: local + setup: [bridge] ders: [pv, battery, meter] control: false size_bytes: 9543 @@ -244,6 +281,7 @@ drivers: version: "2.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 9353 @@ -252,6 +290,7 @@ drivers: version: "1.0.1" tier: community protocol: http + connectivity: local ders: [v2x_charger] control: false size_bytes: 2193 @@ -260,6 +299,8 @@ drivers: version: "0.5.0" tier: core protocol: mqtt + connectivity: local + setup: [bridge] ders: [heatpump] control: true size_bytes: 6973 @@ -268,6 +309,7 @@ drivers: version: "1.1.2" tier: community protocol: "" + connectivity: local ders: [meter] control: false size_bytes: 1362 @@ -276,6 +318,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 15257 @@ -284,6 +327,7 @@ drivers: version: "1.0.5" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 4524 @@ -292,6 +336,7 @@ drivers: version: "1.0.3" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 4620 @@ -300,6 +345,7 @@ drivers: version: "2.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 11199 @@ -308,6 +354,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 3615 @@ -316,6 +363,7 @@ drivers: version: "1.0.3" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 3766 @@ -324,6 +372,8 @@ drivers: version: "1.1.1" tier: core protocol: http + connectivity: cloud + setup: [vendor_portal] ders: [heatpump] control: true size_bytes: 15278 @@ -332,6 +382,8 @@ drivers: version: "1.1.1" tier: core protocol: http + connectivity: local + setup: [device_screen] ders: [heatpump] control: true size_bytes: 18227 @@ -340,6 +392,8 @@ drivers: version: "1.0.2" tier: community protocol: http + connectivity: local + setup: [bridge] ders: [pv, meter] control: false size_bytes: 3766 @@ -348,6 +402,8 @@ drivers: version: "1.0.3" tier: community protocol: mqtt + connectivity: local + setup: [bridge] ders: [pv, meter] control: false size_bytes: 4009 @@ -356,6 +412,7 @@ drivers: version: "1.0.1" tier: community protocol: http + connectivity: local ders: [v2x_charger] control: false size_bytes: 2446 @@ -364,6 +421,7 @@ drivers: version: "2.0.2" tier: core protocol: serial + connectivity: local ders: [meter] control: false size_bytes: 7201 @@ -372,6 +430,7 @@ drivers: version: "2.0.2" tier: core protocol: serial + connectivity: local ders: [meter] control: false size_bytes: 11071 @@ -380,6 +439,7 @@ drivers: version: "2.0.2" tier: core protocol: serial + connectivity: local ders: [meter] control: false size_bytes: 9203 @@ -388,6 +448,7 @@ drivers: version: "2.0.2" tier: core protocol: serial + connectivity: local ders: [meter] control: false size_bytes: 19344 @@ -396,6 +457,7 @@ drivers: version: "2.1.3" tier: core protocol: modbus + connectivity: local ders: [battery, meter] control: false size_bytes: 27853 @@ -404,6 +466,7 @@ drivers: version: "0.3.0" tier: core protocol: mqtt + connectivity: local ders: [pv, meter] control: true size_bytes: 6692 @@ -412,6 +475,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 4855 @@ -420,6 +484,7 @@ drivers: version: "1.0.5" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 4615 @@ -428,6 +493,7 @@ drivers: version: "1.0.3" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 3554 @@ -436,6 +502,7 @@ drivers: version: "2.0.1" tier: core protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 5488 @@ -444,6 +511,7 @@ drivers: version: "1.1.2" tier: community protocol: http + connectivity: local ders: [meter] control: false size_bytes: 11878 @@ -452,6 +520,7 @@ drivers: version: "1.0.5" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 4376 @@ -460,6 +529,7 @@ drivers: version: "1.1.2" tier: community protocol: modbus + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 4841 @@ -468,6 +538,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [battery, meter, pv] control: false size_bytes: 13951 @@ -476,6 +547,8 @@ drivers: version: "1.1.1" tier: core protocol: modbus + connectivity: local + setup: [installer] ders: [pv, meter] control: true size_bytes: 19567 @@ -484,6 +557,7 @@ drivers: version: "1.0.4" tier: community protocol: modbus + connectivity: local ders: [meter] control: false size_bytes: 3818 @@ -492,6 +566,7 @@ drivers: version: "2.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 9083 @@ -500,6 +575,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [meter, pv] control: false size_bytes: 21555 @@ -508,6 +584,7 @@ drivers: version: "0.3.2" tier: core protocol: modbus + connectivity: local ders: [pv] control: true size_bytes: 18246 @@ -516,6 +593,7 @@ drivers: version: "1.2.1" tier: core protocol: modbus + connectivity: local ders: [pv] control: true size_bytes: 12121 @@ -524,6 +602,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 5110 @@ -532,6 +611,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 8837 @@ -540,6 +620,7 @@ drivers: version: "2.1.1" tier: core protocol: modbus + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 17463 @@ -548,6 +629,7 @@ drivers: version: "1.1.1" tier: core protocol: modbus + connectivity: local ders: [pv] control: true size_bytes: 5784 @@ -556,6 +638,8 @@ drivers: version: "2.0.1" tier: community protocol: http + connectivity: local + setup: [device_ui] ders: [battery, meter] control: false size_bytes: 6158 @@ -564,6 +648,7 @@ drivers: version: "1.5.7" tier: core protocol: modbus + connectivity: local ders: [battery, meter, pv] control: true size_bytes: 48889 @@ -572,6 +657,8 @@ drivers: version: "0.2.0" tier: core protocol: http + connectivity: local + setup: [bridge] ders: [vehicle] control: true size_bytes: 23154 @@ -580,6 +667,8 @@ drivers: version: "1.1.0" tier: core protocol: http + connectivity: cloud + setup: [vendor_portal] ders: [meter] control: true size_bytes: 12410 @@ -588,6 +677,7 @@ drivers: version: "1.1.1" tier: community protocol: modbus + connectivity: local ders: [battery, meter] control: false size_bytes: 2794 @@ -596,6 +686,7 @@ drivers: version: "2.1.1" tier: community protocol: modbus + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 7998 @@ -604,6 +695,7 @@ drivers: version: "1.0.3" tier: community protocol: mqtt + connectivity: local ders: [pv, battery, meter] control: false size_bytes: 5779 @@ -612,6 +704,7 @@ drivers: version: "1.0.3" tier: community protocol: modbus + connectivity: local ders: [v2x_charger] control: false size_bytes: 2842 @@ -620,6 +713,7 @@ drivers: version: "2.0.1" tier: core protocol: http + connectivity: local ders: [meter, pv, battery, v2x_charger] control: true size_bytes: 30514 @@ -628,6 +722,7 @@ drivers: version: "1.0.1" tier: community protocol: http + connectivity: local ders: [v2x_charger] control: false size_bytes: 2281 @@ -636,6 +731,8 @@ drivers: version: "1.1.0" tier: core protocol: modbus + connectivity: local + setup: [bridge] ders: [meter] control: true size_bytes: 16217 diff --git a/manifests/abb_meter.yaml b/manifests/abb_meter.yaml index 738b4e1..c939464 100644 --- a/manifests/abb_meter.yaml +++ b/manifests/abb_meter.yaml @@ -3,6 +3,7 @@ version: "1.0.6" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/abb_terra.yaml b/manifests/abb_terra.yaml index 8fd6c82..99b9c90 100644 --- a/manifests/abb_terra.yaml +++ b/manifests/abb_terra.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/acrel.yaml b/manifests/acrel.yaml index 5f8c348..537279a 100644 --- a/manifests/acrel.yaml +++ b/manifests/acrel.yaml @@ -3,6 +3,7 @@ version: "1.0.4" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/alfen.yaml b/manifests/alfen.yaml index fd8cf69..5d78406 100644 --- a/manifests/alfen.yaml +++ b/manifests/alfen.yaml @@ -3,6 +3,7 @@ version: "1.0.4" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/alphaess.yaml b/manifests/alphaess.yaml index 58d6941..ecab864 100644 --- a/manifests/alphaess.yaml +++ b/manifests/alphaess.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/ambibox.yaml b/manifests/ambibox.yaml index 5842dfb..89f09c6 100644 --- a/manifests/ambibox.yaml +++ b/manifests/ambibox.yaml @@ -3,6 +3,7 @@ version: "1.2.0" tier: core author: "Sourceful Labs AB" protocol: mqtt +connectivity: local ders: [battery, meter, v2x_charger] control: true tested_devices: diff --git a/manifests/ambibox_v2x.yaml b/manifests/ambibox_v2x.yaml index 318f041..a426ab3 100644 --- a/manifests/ambibox_v2x.yaml +++ b/manifests/ambibox_v2x.yaml @@ -3,6 +3,7 @@ version: "1.1.0" tier: core author: "Sourceful Labs AB" protocol: mqtt +connectivity: local ders: [v2x_charger] control: true tested_devices: diff --git a/manifests/atmoce.yaml b/manifests/atmoce.yaml index f761875..3c778dd 100644 --- a/manifests/atmoce.yaml +++ b/manifests/atmoce.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/carlo_gavazzi.yaml b/manifests/carlo_gavazzi.yaml index a3dbcc7..c218f08 100644 --- a/manifests/carlo_gavazzi.yaml +++ b/manifests/carlo_gavazzi.yaml @@ -3,6 +3,7 @@ version: "1.0.5" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/chint.yaml b/manifests/chint.yaml index 9a432fb..622d13c 100644 --- a/manifests/chint.yaml +++ b/manifests/chint.yaml @@ -3,6 +3,7 @@ version: "1.0.5" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/circutor.yaml b/manifests/circutor.yaml index 4c22f76..0297174 100644 --- a/manifests/circutor.yaml +++ b/manifests/circutor.yaml @@ -3,6 +3,7 @@ version: "1.0.5" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/ctek.yaml b/manifests/ctek.yaml index beaab2e..50782cc 100644 --- a/manifests/ctek.yaml +++ b/manifests/ctek.yaml @@ -3,6 +3,8 @@ version: "0.3.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local +setup: [device_ui] ders: [ev] control: true tested_devices: diff --git a/manifests/ctek_hybrid.yaml b/manifests/ctek_hybrid.yaml index 594caee..f5c97e9 100644 --- a/manifests/ctek_hybrid.yaml +++ b/manifests/ctek_hybrid.yaml @@ -3,6 +3,8 @@ version: "0.3.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local +setup: [device_ui] ders: [ev] control: true tested_devices: diff --git a/manifests/ctek_v2.yaml b/manifests/ctek_v2.yaml index ec19184..e167712 100644 --- a/manifests/ctek_v2.yaml +++ b/manifests/ctek_v2.yaml @@ -3,6 +3,8 @@ version: "0.3.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local +setup: [device_ui] ders: [ev] control: true tested_devices: diff --git a/manifests/deye.yaml b/manifests/deye.yaml index 7c29124..2211f2a 100644 --- a/manifests/deye.yaml +++ b/manifests/deye.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: false tested_devices: diff --git a/manifests/easee.yaml b/manifests/easee.yaml index 6e52490..d6ba07c 100644 --- a/manifests/easee.yaml +++ b/manifests/easee.yaml @@ -3,6 +3,7 @@ version: "1.0.4" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index 535c7ff..4916c5f 100644 --- a/manifests/easee_cloud.yaml +++ b/manifests/easee_cloud.yaml @@ -3,6 +3,8 @@ version: "1.1.0" tier: core author: "Sourceful Labs AB" protocol: http +connectivity: cloud +setup: [vendor_portal] ders: [ev] control: true tested_devices: diff --git a/manifests/esphome-dsmr.yaml b/manifests/esphome-dsmr.yaml index de34f28..a3e28e0 100644 --- a/manifests/esphome-dsmr.yaml +++ b/manifests/esphome-dsmr.yaml @@ -3,6 +3,8 @@ version: "1.0.3" tier: community author: "FTW contributors" protocol: http +connectivity: local +setup: [bridge] ders: [meter] control: false tested_devices: diff --git a/manifests/esphome_dsmr.yaml b/manifests/esphome_dsmr.yaml index d772c0a..a6c0ade 100644 --- a/manifests/esphome_dsmr.yaml +++ b/manifests/esphome_dsmr.yaml @@ -3,6 +3,8 @@ version: "1.0.3" tier: core author: "Sourceful Labs AB" protocol: http +connectivity: local +setup: [bridge] ders: [meter] control: true tested_devices: diff --git a/manifests/etrel.yaml b/manifests/etrel.yaml index 3ca702c..b908e63 100644 --- a/manifests/etrel.yaml +++ b/manifests/etrel.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/ferroamp.yaml b/manifests/ferroamp.yaml index 9c30a2e..38d6e7e 100644 --- a/manifests/ferroamp.yaml +++ b/manifests/ferroamp.yaml @@ -3,6 +3,7 @@ version: "2.1.0" tier: core author: "Sourceful Labs AB" protocol: mqtt +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/ferroamp_dc2_v2x.yaml b/manifests/ferroamp_dc2_v2x.yaml index da83ad9..9161c1b 100644 --- a/manifests/ferroamp_dc2_v2x.yaml +++ b/manifests/ferroamp_dc2_v2x.yaml @@ -3,6 +3,7 @@ version: "2.1.0" tier: core author: "Sourceful Labs AB" protocol: mqtt +connectivity: local ders: [v2x_charger] control: true tested_devices: diff --git a/manifests/ferroamp_modbus.yaml b/manifests/ferroamp_modbus.yaml index 5b499a1..5f93569 100644 --- a/manifests/ferroamp_modbus.yaml +++ b/manifests/ferroamp_modbus.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/foxess.yaml b/manifests/foxess.yaml index bee3b97..d717b13 100644 --- a/manifests/foxess.yaml +++ b/manifests/foxess.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/fronius.yaml b/manifests/fronius.yaml index b8cff23..2395f81 100644 --- a/manifests/fronius.yaml +++ b/manifests/fronius.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: false tested_devices: diff --git a/manifests/fronius_api.yaml b/manifests/fronius_api.yaml index f5ef108..88aa841 100644 --- a/manifests/fronius_api.yaml +++ b/manifests/fronius_api.yaml @@ -3,6 +3,7 @@ version: "1.0.2" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local ders: [pv, meter] control: false tested_devices: diff --git a/manifests/fronius_smart_meter.yaml b/manifests/fronius_smart_meter.yaml index 991088e..3d88d58 100644 --- a/manifests/fronius_smart_meter.yaml +++ b/manifests/fronius_smart_meter.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/goe.yaml b/manifests/goe.yaml index 2421310..ab17290 100644 --- a/manifests/goe.yaml +++ b/manifests/goe.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/goe_http.yaml b/manifests/goe_http.yaml index 79529e7..ab313bc 100644 --- a/manifests/goe_http.yaml +++ b/manifests/goe_http.yaml @@ -3,6 +3,7 @@ version: "1.0.1" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/goodwe.yaml b/manifests/goodwe.yaml index df4c59f..8eb6abd 100644 --- a/manifests/goodwe.yaml +++ b/manifests/goodwe.yaml @@ -3,6 +3,8 @@ version: "2.1.1" tier: community author: "frekes81 and Sourceful contributors" protocol: modbus +connectivity: local +setup: [bridge] ders: [pv, battery, meter] control: false tested_devices: [] diff --git a/manifests/growatt.yaml b/manifests/growatt.yaml index 3c9f73c..8375288 100644 --- a/manifests/growatt.yaml +++ b/manifests/growatt.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/hardybarth.yaml b/manifests/hardybarth.yaml index 498fd46..7288aa7 100644 --- a/manifests/hardybarth.yaml +++ b/manifests/hardybarth.yaml @@ -3,6 +3,7 @@ version: "1.0.1" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/heishamon.yaml b/manifests/heishamon.yaml index 85d0263..3363c4f 100644 --- a/manifests/heishamon.yaml +++ b/manifests/heishamon.yaml @@ -3,6 +3,8 @@ version: "0.5.0" tier: core author: "Sourceful Labs AB" protocol: mqtt +connectivity: local +setup: [bridge] ders: [heatpump] control: true tested_devices: diff --git a/manifests/hello.yaml b/manifests/hello.yaml index 5c98ab6..19bb79d 100644 --- a/manifests/hello.yaml +++ b/manifests/hello.yaml @@ -3,6 +3,7 @@ version: "1.1.2" tier: community author: "" protocol: "" +connectivity: local ders: [meter] control: false tested_devices: [] diff --git a/manifests/huawei.yaml b/manifests/huawei.yaml index 6842f9c..aef4e01 100644 --- a/manifests/huawei.yaml +++ b/manifests/huawei.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/janitza.yaml b/manifests/janitza.yaml index 6c06a97..ba1822e 100644 --- a/manifests/janitza.yaml +++ b/manifests/janitza.yaml @@ -3,6 +3,7 @@ version: "1.0.5" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/keba.yaml b/manifests/keba.yaml index 853bfcc..101f35d 100644 --- a/manifests/keba.yaml +++ b/manifests/keba.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/kostal.yaml b/manifests/kostal.yaml index 42730b2..501807e 100644 --- a/manifests/kostal.yaml +++ b/manifests/kostal.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/kstar.yaml b/manifests/kstar.yaml index c788ad1..d61c05c 100644 --- a/manifests/kstar.yaml +++ b/manifests/kstar.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/mennekes.yaml b/manifests/mennekes.yaml index 5935236..678a463 100644 --- a/manifests/mennekes.yaml +++ b/manifests/mennekes.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/myuplink.yaml b/manifests/myuplink.yaml index 5f82e6d..03a17c5 100644 --- a/manifests/myuplink.yaml +++ b/manifests/myuplink.yaml @@ -3,6 +3,8 @@ version: "1.1.1" tier: core author: "Sourceful Labs AB" protocol: http +connectivity: cloud +setup: [vendor_portal] ders: [heatpump] control: true tested_devices: diff --git a/manifests/nibe_local.yaml b/manifests/nibe_local.yaml index b20df41..960b9ad 100644 --- a/manifests/nibe_local.yaml +++ b/manifests/nibe_local.yaml @@ -3,6 +3,8 @@ version: "1.1.1" tier: core author: "HuggeK with the help of Claude Code" protocol: http +connectivity: local +setup: [device_screen] ders: [heatpump] control: true tested_devices: diff --git a/manifests/opendtu.yaml b/manifests/opendtu.yaml index 09daee6..420a0f1 100644 --- a/manifests/opendtu.yaml +++ b/manifests/opendtu.yaml @@ -3,6 +3,8 @@ version: "1.0.2" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local +setup: [bridge] ders: [pv, meter] control: false tested_devices: diff --git a/manifests/opendtu_mqtt.yaml b/manifests/opendtu_mqtt.yaml index 3c33348..526f0a3 100644 --- a/manifests/opendtu_mqtt.yaml +++ b/manifests/opendtu_mqtt.yaml @@ -3,6 +3,8 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: mqtt +connectivity: local +setup: [bridge] ders: [pv, meter] control: false tested_devices: diff --git a/manifests/openevse.yaml b/manifests/openevse.yaml index 3838844..4b2aea7 100644 --- a/manifests/openevse.yaml +++ b/manifests/openevse.yaml @@ -3,6 +3,7 @@ version: "1.0.1" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/p1_dsmr.yaml b/manifests/p1_dsmr.yaml index 3b24ccc..b8c0ed6 100644 --- a/manifests/p1_dsmr.yaml +++ b/manifests/p1_dsmr.yaml @@ -3,6 +3,7 @@ version: "2.0.2" tier: core author: "Sourceful Labs AB" protocol: serial +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/p1_encrypted.yaml b/manifests/p1_encrypted.yaml index 6facc91..5eca352 100644 --- a/manifests/p1_encrypted.yaml +++ b/manifests/p1_encrypted.yaml @@ -3,6 +3,7 @@ version: "2.0.2" tier: core author: "Sourceful Labs AB" protocol: serial +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/p1_hdlc.yaml b/manifests/p1_hdlc.yaml index 4a50171..f171670 100644 --- a/manifests/p1_hdlc.yaml +++ b/manifests/p1_hdlc.yaml @@ -3,6 +3,7 @@ version: "2.0.2" tier: core author: "Sourceful Labs AB" protocol: serial +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/p1_meter.yaml b/manifests/p1_meter.yaml index b46ee78..1147434 100644 --- a/manifests/p1_meter.yaml +++ b/manifests/p1_meter.yaml @@ -3,6 +3,7 @@ version: "2.0.2" tier: core author: "Sourceful Labs AB" protocol: serial +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/pixii.yaml b/manifests/pixii.yaml index bcaa567..429fc58 100644 --- a/manifests/pixii.yaml +++ b/manifests/pixii.yaml @@ -3,6 +3,7 @@ version: "2.1.3" tier: core author: "Sourceful Labs AB and community contributors" protocol: modbus +connectivity: local ders: [battery, meter] control: false tested_devices: diff --git a/manifests/pixii_pv.yaml b/manifests/pixii_pv.yaml index 1fd21eb..26967cf 100644 --- a/manifests/pixii_pv.yaml +++ b/manifests/pixii_pv.yaml @@ -3,6 +3,7 @@ version: "0.3.0" tier: core author: "Sourceful Labs AB" protocol: mqtt +connectivity: local ders: [pv, meter] control: true tested_devices: diff --git a/manifests/saj.yaml b/manifests/saj.yaml index 3bd1483..0e7724a 100644 --- a/manifests/saj.yaml +++ b/manifests/saj.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/schneider_meter.yaml b/manifests/schneider_meter.yaml index cae861e..e98fcc1 100644 --- a/manifests/schneider_meter.yaml +++ b/manifests/schneider_meter.yaml @@ -3,6 +3,7 @@ version: "1.0.5" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/schrack_ev.yaml b/manifests/schrack_ev.yaml index ab443c5..be851a8 100644 --- a/manifests/schrack_ev.yaml +++ b/manifests/schrack_ev.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/sdm630.yaml b/manifests/sdm630.yaml index f48f800..2cf04a4 100644 --- a/manifests/sdm630.yaml +++ b/manifests/sdm630.yaml @@ -3,6 +3,7 @@ version: "2.0.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/shelly.yaml b/manifests/shelly.yaml index 43156be..9e500b2 100644 --- a/manifests/shelly.yaml +++ b/manifests/shelly.yaml @@ -3,6 +3,7 @@ version: "1.1.2" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/siemens_pac.yaml b/manifests/siemens_pac.yaml index a10fddc..1f60325 100644 --- a/manifests/siemens_pac.yaml +++ b/manifests/siemens_pac.yaml @@ -3,6 +3,7 @@ version: "1.0.5" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/sigenergy.yaml b/manifests/sigenergy.yaml index 1c318f6..dec5550 100644 --- a/manifests/sigenergy.yaml +++ b/manifests/sigenergy.yaml @@ -3,6 +3,7 @@ version: "1.1.2" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/sma.yaml b/manifests/sma.yaml index def2d36..7bd64fb 100644 --- a/manifests/sma.yaml +++ b/manifests/sma.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: false tested_devices: diff --git a/manifests/sma_pv.yaml b/manifests/sma_pv.yaml index 93bdf0d..703967b 100644 --- a/manifests/sma_pv.yaml +++ b/manifests/sma_pv.yaml @@ -3,6 +3,8 @@ version: "1.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local +setup: [installer] ders: [pv, meter] control: true tested_devices: diff --git a/manifests/socomec.yaml b/manifests/socomec.yaml index 73d6658..8653063 100644 --- a/manifests/socomec.yaml +++ b/manifests/socomec.yaml @@ -3,6 +3,7 @@ version: "1.0.4" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter] control: false tested_devices: diff --git a/manifests/sofar.yaml b/manifests/sofar.yaml index cd22b57..b14db25 100644 --- a/manifests/sofar.yaml +++ b/manifests/sofar.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/solaredge.yaml b/manifests/solaredge.yaml index e4d7063..992acb8 100644 --- a/manifests/solaredge.yaml +++ b/manifests/solaredge.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [meter, pv] control: false tested_devices: diff --git a/manifests/solaredge_legacy.yaml b/manifests/solaredge_legacy.yaml index b7ec48c..b9b04a7 100644 --- a/manifests/solaredge_legacy.yaml +++ b/manifests/solaredge_legacy.yaml @@ -3,6 +3,7 @@ version: "0.3.2" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv] control: true tested_devices: diff --git a/manifests/solaredge_pv.yaml b/manifests/solaredge_pv.yaml index 10d3d56..2f0f2c1 100644 --- a/manifests/solaredge_pv.yaml +++ b/manifests/solaredge_pv.yaml @@ -3,6 +3,7 @@ version: "1.2.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv] control: true tested_devices: diff --git a/manifests/solax.yaml b/manifests/solax.yaml index b2edd48..de5f487 100644 --- a/manifests/solax.yaml +++ b/manifests/solax.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/solinteg.yaml b/manifests/solinteg.yaml index 409a697..731ff4c 100644 --- a/manifests/solinteg.yaml +++ b/manifests/solinteg.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/solis.yaml b/manifests/solis.yaml index e3c2840..2421417 100644 --- a/manifests/solis.yaml +++ b/manifests/solis.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/solis_string.yaml b/manifests/solis_string.yaml index edbb628..8b0ba0c 100644 --- a/manifests/solis_string.yaml +++ b/manifests/solis_string.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv] control: true tested_devices: diff --git a/manifests/sonnen.yaml b/manifests/sonnen.yaml index f0ff588..5ecbfd3 100644 --- a/manifests/sonnen.yaml +++ b/manifests/sonnen.yaml @@ -3,6 +3,8 @@ version: "2.0.1" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local +setup: [device_ui] ders: [battery, meter] control: false tested_devices: diff --git a/manifests/sungrow.yaml b/manifests/sungrow.yaml index 88e5577..3d9e3fb 100644 --- a/manifests/sungrow.yaml +++ b/manifests/sungrow.yaml @@ -3,6 +3,7 @@ version: "1.5.7" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: diff --git a/manifests/tesla_vehicle.yaml b/manifests/tesla_vehicle.yaml index 73616b5..55d1fdc 100644 --- a/manifests/tesla_vehicle.yaml +++ b/manifests/tesla_vehicle.yaml @@ -3,6 +3,8 @@ version: "0.2.0" tier: core author: "Sourceful Labs AB" protocol: http +connectivity: local +setup: [bridge] ders: [vehicle] control: true tested_devices: diff --git a/manifests/tibber.yaml b/manifests/tibber.yaml index 88feedf..7ed6da5 100644 --- a/manifests/tibber.yaml +++ b/manifests/tibber.yaml @@ -3,6 +3,8 @@ version: "1.1.0" tier: core author: "Sourceful Labs AB" protocol: http +connectivity: cloud +setup: [vendor_portal] ders: [meter] control: true tested_devices: diff --git a/manifests/varta.yaml b/manifests/varta.yaml index 8df47af..887ba9c 100644 --- a/manifests/varta.yaml +++ b/manifests/varta.yaml @@ -3,6 +3,7 @@ version: "1.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter] control: false tested_devices: diff --git a/manifests/victron.yaml b/manifests/victron.yaml index 67e3149..e84e5b1 100644 --- a/manifests/victron.yaml +++ b/manifests/victron.yaml @@ -3,6 +3,7 @@ version: "2.1.1" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/victron_mqtt.yaml b/manifests/victron_mqtt.yaml index 37ddfae..f3d24e4 100644 --- a/manifests/victron_mqtt.yaml +++ b/manifests/victron_mqtt.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: mqtt +connectivity: local ders: [pv, battery, meter] control: false tested_devices: diff --git a/manifests/wallbox.yaml b/manifests/wallbox.yaml index b327d65..d705fa1 100644 --- a/manifests/wallbox.yaml +++ b/manifests/wallbox.yaml @@ -3,6 +3,7 @@ version: "1.0.3" tier: community author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/zap.yaml b/manifests/zap.yaml index 1254b3b..7e5610e 100644 --- a/manifests/zap.yaml +++ b/manifests/zap.yaml @@ -3,6 +3,7 @@ version: "2.0.1" tier: core author: "Sourceful Labs AB" protocol: http +connectivity: local ders: [meter, pv, battery, v2x_charger] control: true tested_devices: diff --git a/manifests/zaptec.yaml b/manifests/zaptec.yaml index a090267..f0d57f6 100644 --- a/manifests/zaptec.yaml +++ b/manifests/zaptec.yaml @@ -3,6 +3,7 @@ version: "1.0.1" tier: community author: "Sourceful Labs AB" protocol: http +connectivity: local ders: [v2x_charger] control: false tested_devices: diff --git a/manifests/zuidwijk_p1.yaml b/manifests/zuidwijk_p1.yaml index 0d8245f..e35158f 100644 --- a/manifests/zuidwijk_p1.yaml +++ b/manifests/zuidwijk_p1.yaml @@ -3,6 +3,8 @@ version: "1.1.0" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local +setup: [bridge] ders: [meter] control: true tested_devices: diff --git a/spec/manifest-v2.md b/spec/manifest-v2.md index c1c76a6..cf1bc8f 100644 --- a/spec/manifest-v2.md +++ b/spec/manifest-v2.md @@ -11,6 +11,8 @@ Each driver has a YAML manifest in `manifests/` that describes its metadata, cap | `tier` | string | Yes | One of: `core`, `community`, `oem` | | `author` | string | Yes (core) | Author name or organization | | `protocol` | string | Yes | `modbus`, `mqtt`, `serial`, `standalone`, or `""` | +| `connectivity` | string | Yes | `local` or `cloud` — where the driver talks while running | +| `setup` | list | No | What a human must obtain before it connects (see below) | | `ders` | list | Yes | DER types the driver covers (see below) | | `control` | bool | Yes | Whether the driver supports EMS control commands | | `tested_devices` | list | No | Devices tested against (see below) | @@ -25,6 +27,52 @@ Each driver has a YAML manifest in `manifests/` that describes its metadata, cap | `bytecode_size` | int | No | Size of the `.luac` bytecode file in bytes | | `changelog` | string | No | Version-specific release notes | +## Connectivity and Setup + +`protocol` names a transport, which does not answer the two questions an owner +asks first. `nibe_local` and `myuplink` are both `protocol: http` and both read +a NIBE heat pump, and they are opposites: one reads the pump over the LAN, the +other reads NIBE's cloud. + +`connectivity` says where the driver talks while it is running. + +| Value | Meaning | +|-------|---------| +| `local` | Talks only to the device or a broker on the local network. Keeps working with the internet unplugged | +| `cloud` | Talks to a vendor service over the internet. No internet, no telemetry | + +`setup` says what a human must obtain **once**, and from whom, before the +driver can connect at all — which is a separate question, because a driver can +be `local` and still need a manufacturer to unlock the interface first. + +| Value | Meaning | +|-------|---------| +| `none` | Nothing beyond reaching the device on the network | +| `device_screen` | Enabled on the device's own display or keypad | +| `device_ui` | Enabled in the device's own local web interface | +| `vendor_app` | Requires the manufacturer's phone app | +| `vendor_portal` | Requires an account, API key or OAuth app from the vendor | +| `installer` | Requires installer-level access or a service partner | +| `vendor_approval` | The manufacturer must enable it for this site on request | +| `bridge` | Requires separate hardware or firmware in between | + +An **absent** `setup` means nobody has recorded one. It does not mean there is +nothing to do, and the catalog page renders it as "Not recorded" rather than +"Nothing to set up". Only `setup: [none]` claims a device needs nothing but an +address, so state it only when that has been confirmed. `none` cannot be +combined with another value. + +`bridge` is for a translator that is not the device and not the vendor's own +gateway product: a HeishaMon board, OpenDTU, an ESPHome build, TeslaBleHttpProxy, +a serial-to-Ethernet bridge. A vendor gateway the customer buys as part of the +system — the Sourceful Zap, a Ferroamp EnergyHub — is the device, not a bridge. + +Neither field reaches the signed driver artifact: `ftw_repository._load_channel` +copies an explicit allowlist (`ders`, `protocol`, `manufacturer`, `model_family` +and the `DRIVER` table) into the published metadata, and these two are not in +it. So correcting one does **not** need a version bump, unlike the fields listed +in CONTRIBUTING. + ## DER Types `ders` states which kinds of distributed energy resource a driver covers. @@ -116,6 +164,7 @@ version: "1.1.0" tier: core author: "Sourceful Labs AB" protocol: modbus +connectivity: local ders: [battery, meter, pv] control: true tested_devices: @@ -152,11 +201,13 @@ Version changes must be accompanied by a `CHANGELOG.md` entry under `[Unreleased 3. `tier` must be one of the three valid tiers 4. `protocol` must be a known protocol or empty string 5. All entries in `ders` must be valid DER types -6. `size_bytes` must be non-negative -7. `core` tier drivers must have an `author` -8. Every manifest must have a corresponding `.lua` file in `drivers/` -9. `tested_devices` entries must have `manufacturer` and `model_family` -10. `upstream_docs` entries must have an `http(s)` `url`; `kind` and `url_stability`, when present, must be known values +6. `connectivity` must be `local` or `cloud` +7. `setup`, when present, must be a non-empty list of valid values, and `none` must stand alone +8. `size_bytes` must be non-negative +9. `core` tier drivers must have an `author` +10. Every manifest must have a corresponding `.lua` file in `drivers/` +11. `tested_devices` entries must have `manufacturer` and `model_family` +12. `upstream_docs` entries must have an `http(s)` `url`; `kind` and `url_stability`, when present, must be known values ## Migration from V1 @@ -170,4 +221,6 @@ V2.2 adds bytecode fields: `bytecode_sha256`, `bytecode_signature`, `bytecode_si V2.3 adds `upstream_docs`: an optional list of vendor reference documents (`url`, `title`, `kind`, `url_stability`) to watch for register/protocol changes. +V2.4 adds `connectivity` (`local`/`cloud`) and the optional `setup` list: where a driver talks while it is running, and what a human must obtain once before it can connect at all. `connectivity` is required, so every manifest carries it. + Use `tools/migrate_manifests.py` to convert V1 JSON → V2 YAML. diff --git a/tests/test_connectivity.py b/tests/test_connectivity.py new file mode 100644 index 0000000..7093d88 --- /dev/null +++ b/tests/test_connectivity.py @@ -0,0 +1,113 @@ +"""`connectivity` has to be checkable, or it is just a word in a file. + +A driver that reaches a vendor service has to name that service somewhere in +its source: it builds a URL from a hostname it hardcodes, because there is +nowhere else for a cloud endpoint to come from. A driver that only talks to +the LAN takes its address from config. That asymmetry is what these tests read, +so a manifest cannot claim `local` while the Lua calls an API on the internet. + +`setup` cannot be derived the same way -- nothing in the source proves a +manufacturer has to unlock an interface first -- so it is checked for shape +and for the one rule that carries meaning: absent is not `none`. +""" + +import re +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +MANIFESTS = sorted((ROOT / "manifests").glob("*.yaml")) + +# A URL literal in code, not in a comment and not the DRIVER table's homepage. +URL_RE = re.compile(r"""https?://([A-Za-z][A-Za-z0-9.-]*\.[A-Za-z]{2,})""") +COMMENT_RE = re.compile(r"^\s*--") +HOMEPAGE_RE = re.compile(r"^\s*homepage\s*=") + + +def manifest_field(text: str, field: str) -> str: + match = re.search(rf"^{field}:\s*(.+)$", text, re.MULTILINE) + return match.group(1).strip().strip('"') if match else "" + + +def manifest_list(text: str, field: str) -> list[str] | None: + match = re.search(rf"^{field}:\s*\[(.*)\]$", text, re.MULTILINE) + if not match: + return None + return [item.strip() for item in match.group(1).split(",") if item.strip()] + + +def called_hosts(driver_id: str) -> set[str]: + """Hostnames the driver hardcodes into a request, ignoring documentation.""" + source = ROOT / "drivers" / "lua" / f"{driver_id}.lua" + if not source.is_file(): + return set() + hosts: set[str] = set() + for line in source.read_text(encoding="utf-8").splitlines(): + if COMMENT_RE.match(line) or HOMEPAGE_RE.match(line): + continue + for host in URL_RE.findall(line): + # A link to a project page is provenance, not an endpoint. + if host.endswith("github.com") or host.startswith("developer."): + continue + hosts.add(host) + return hosts + + +IDS = [path.stem for path in MANIFESTS] + + +@pytest.mark.parametrize("driver_id", IDS) +def test_every_driver_states_where_it_talks(driver_id: str) -> None: + text = (ROOT / "manifests" / f"{driver_id}.yaml").read_text(encoding="utf-8") + assert manifest_field(text, "connectivity") in {"local", "cloud"}, ( + f"{driver_id}: connectivity must be local or cloud") + + +@pytest.mark.parametrize("driver_id", IDS) +def test_a_local_driver_does_not_call_the_internet(driver_id: str) -> None: + text = (ROOT / "manifests" / f"{driver_id}.yaml").read_text(encoding="utf-8") + if manifest_field(text, "connectivity") != "local": + return + hosts = called_hosts(driver_id) + assert not hosts, ( + f"{driver_id} claims connectivity: local but its source builds requests " + f"against {sorted(hosts)}") + + +@pytest.mark.parametrize("driver_id", IDS) +def test_a_cloud_driver_names_the_service_it_needs(driver_id: str) -> None: + text = (ROOT / "manifests" / f"{driver_id}.yaml").read_text(encoding="utf-8") + if manifest_field(text, "connectivity") != "cloud": + return + assert called_hosts(driver_id), ( + f"{driver_id} claims connectivity: cloud but hardcodes no vendor host; " + "if it reads a device on the LAN it is local") + + +@pytest.mark.parametrize("driver_id", IDS) +def test_setup_is_absent_or_meaningful(driver_id: str) -> None: + text = (ROOT / "manifests" / f"{driver_id}.yaml").read_text(encoding="utf-8") + setup = manifest_list(text, "setup") + if setup is None: + assert not re.search(r"^setup:", text, re.MULTILINE), ( + f"{driver_id}: setup must be an inline list, e.g. setup: [device_ui]") + return + assert setup, f"{driver_id}: omit setup rather than stating an empty list" + assert "none" not in setup or len(setup) == 1, ( + f"{driver_id}: setup 'none' cannot be combined with another requirement") + + +def test_a_cloud_driver_needs_something_from_its_vendor() -> None: + """Reaching a vendor service means holding a credential for it. + + Not a style rule: a cloud driver whose setup is unrecorded is the case + where an owner discovers only after installing that they need an account. + """ + for path in MANIFESTS: + text = path.read_text(encoding="utf-8") + if manifest_field(text, "connectivity") != "cloud": + continue + setup = manifest_list(text, "setup") or [] + assert setup and setup != ["none"], ( + f"{path.stem}: a cloud driver must record what its vendor requires") diff --git a/tools/generate_index.py b/tools/generate_index.py index 0901387..c6adf4e 100644 --- a/tools/generate_index.py +++ b/tools/generate_index.py @@ -53,6 +53,8 @@ def main(): "version": data.get("version", "1.0.0"), "tier": data.get("tier", "community"), "protocol": data.get("protocol", ""), + "connectivity": data.get("connectivity", ""), + "setup": data.get("setup", []), "ders": data.get("ders", []), "control": data.get("control", False), "size_bytes": size_bytes, @@ -68,6 +70,11 @@ def main(): lines.append(f' version: "{e["version"]}"') lines.append(f' tier: {e["tier"]}') lines.append(f' protocol: {e["protocol"]}' if e["protocol"] else ' protocol: ""') + lines.append(f' connectivity: {e["connectivity"]}') + # Omitted rather than empty: no recorded setup requirement is not a + # claim that a device needs none. + if e["setup"]: + lines.append(f' setup: {yaml_list_inline(e["setup"])}') lines.append(f' ders: {yaml_list_inline(e["ders"])}') lines.append(f' control: {"true" if e["control"] else "false"}') lines.append(f' size_bytes: {e["size_bytes"]}') diff --git a/tools/generate_site.py b/tools/generate_site.py index 3b3bcba..9d6c42b 100644 --- a/tools/generate_site.py +++ b/tools/generate_site.py @@ -42,6 +42,27 @@ "v2x_charger": "V2X charger", "vehicle": "Vehicle", } +# Where the driver talks while it runs. The question behind this filter is +# "does this keep working when the internet is down?", so the labels answer +# that rather than naming a transport. +CONNECTIVITY_LABELS = { + "local": "Local only", + "cloud": "Needs the internet", + "": "Unspecified", +} +# What somebody has to obtain before any of it works, even when the driver +# itself never leaves the LAN. A driver with no entry has had none recorded, +# which the page says instead of implying there is nothing to do. +SETUP_LABELS = { + "none": "Nothing to set up", + "device_screen": "Enable on the device screen", + "device_ui": "Enable in the device web UI", + "vendor_app": "Manufacturer app", + "vendor_portal": "Manufacturer account", + "installer": "Installer access", + "vendor_approval": "Manufacturer must enable it", + "bridge": "Needs a bridge device", +} PROTOCOL_LABELS = { "modbus": "Modbus", "mqtt": "MQTT", @@ -267,6 +288,8 @@ def collect_drivers() -> list[dict]: "version": data.get("version", ""), "tier": data.get("tier", "community"), "protocol": data.get("protocol", ""), + "connectivity": data.get("connectivity", ""), + "setup": data.get("setup", []) or [], "ders": data.get("ders", []) or [], "control": bool(data.get("control", False)), "author": data.get("author", ""), @@ -319,6 +342,8 @@ def build_catalog() -> dict: "ders": DER_LABELS, "protocols": PROTOCOL_LABELS, "tiers": TIER_LABELS, + "connectivity": CONNECTIVITY_LABELS, + "setup": SETUP_LABELS, "doc_kinds": DOC_KIND_LABELS, "url_stability": URL_STABILITY_LABELS, }, @@ -431,7 +456,7 @@ def embed_json(payload: dict) -> str: .catalog-body { padding-top: 30px; } .driver { border: 1px solid var(--line); background: var(--surface); margin-bottom: -1px; } .driver.is-open { border-color: var(--ink); position: relative; z-index: 1; } -.driver-head { width: 100%; display: grid; grid-template-columns: minmax(0, 3fr) 90px minmax(0, 1.6fr) 76px 92px 74px 22px; align-items: center; gap: 14px; padding: 15px 18px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; } +.driver-head { width: 100%; display: grid; grid-template-columns: minmax(0, 3fr) 90px 74px minmax(0, 1.6fr) 76px 92px 74px 22px; align-items: center; gap: 14px; padding: 15px 18px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; } .driver-head:hover { background: var(--signal-light); } .driver-id { display: block; color: var(--ink); font-family: var(--mono); font-size: 14px; font-weight: 700; } .driver-title { display: block; margin-top: 2px; color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -440,6 +465,9 @@ def embed_json(payload: dict) -> str: .badge.tier-core { border-color: var(--ink); background: var(--ink); color: var(--cream); } .badge.control-yes { border-color: var(--signal); color: var(--signal); font-weight: 700; } .badge.control-no { color: var(--muted); opacity: 0.7; } +/* Local-first is the default this project is built for, so the badge that + earns attention is the one that says a device stops without the internet. */ +.badge.conn-cloud { border-style: dotted; color: var(--muted); } .ders { display: flex; gap: 5px; flex-wrap: wrap; } .ders .badge { border-style: dashed; } .version { color: var(--ink); font-family: var(--mono); font-size: 12px; } @@ -532,7 +560,8 @@ def embed_json(payload: dict) -> str: var drivers = CATALOG.drivers; var labels = CATALOG.labels; var state = { view: "drivers", query: "", ders: new Set(), protocols: new Set(), - tiers: new Set(), control: false, verified: false }; + tiers: new Set(), connectivity: new Set(), setup: new Set(), + control: false, verified: false }; var listEl = document.getElementById("list"); var countEl = document.getElementById("result-count"); @@ -546,6 +575,8 @@ def embed_json(payload: dict) -> str: function derLabel(d) { return labels.ders[d] || d; } function protocolLabel(p) { return labels.protocols[p] || p; } function tierLabel(t) { return labels.tiers[t] || t; } + function connectivityLabel(c) { return labels.connectivity[c] || c; } + function setupLabel(s) { return labels.setup[s] || s; } function docKindLabel(k) { return labels.doc_kinds[k] || k; } function stabilityLabel(s) { return labels.url_stability[s] || s; } function bytes(n) { return n >= 1024 ? (n / 1024).toFixed(1) + " kB" : n + " B"; } @@ -554,6 +585,8 @@ def embed_json(payload: dict) -> str: // manufacturer, not only the driver id. drivers.forEach(function (d) { var parts = [d.id, d.title, d.description, d.protocol, d.tier, d.author]; + parts.push(d.connectivity, connectivityLabel(d.connectivity)); + d.setup.forEach(function (x) { parts.push(x, setupLabel(x)); }); d.ders.forEach(function (x) { parts.push(x, derLabel(x)); }); d.manufacturers.forEach(function (m) { parts.push(m); }); d.tested_devices.forEach(function (dev) { @@ -570,6 +603,13 @@ def embed_json(payload: dict) -> str: if (state.verified && !d.hardware_verified) return false; if (state.tiers.size && !state.tiers.has(d.tier)) return false; if (state.protocols.size && !state.protocols.has(d.protocol)) return false; + if (state.connectivity.size && !state.connectivity.has(d.connectivity)) return false; + // A driver with no recorded setup requirement cannot answer this filter, + // so it drops out rather than passing as if it needed nothing. + if (state.setup.size) { + var setupHit = d.setup.some(function (x) { return state.setup.has(x); }); + if (!setupHit) return false; + } if (state.ders.size) { var hit = d.ders.some(function (x) { return state.ders.has(x); }); if (!hit) return false; @@ -638,6 +678,10 @@ def embed_json(payload: dict) -> str: var control = d.control ? 'Control' : 'Read only'; + var reach = d.connectivity + ? '' + + (d.connectivity === "cloud" ? "Cloud" : "Local") + "" + : ""; // The driver's own verification_status, rendered as what it actually says. // Most drivers are ports awaiting hardware, and the page must not dress // that up as a test result. @@ -655,6 +699,10 @@ def embed_json(payload: dict) -> str: var facts = [ ["Tier", esc(tierLabel(d.tier))], ["Protocol", esc(protocolLabel(d.protocol))], + ["Reach", esc(connectivityLabel(d.connectivity))], + ["To set up", d.setup.length + ? d.setup.map(function (x) { return esc(setupLabel(x)); }).join(", ") + : "Not recorded"], ["Catalog version", esc(d.version)], ["Min host", esc(d.min_host_version || "—")], ["Source size", bytes(d.size_bytes)], @@ -670,6 +718,7 @@ def embed_json(payload: dict) -> str: '