diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f48ea1..377fc9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ### Changed +- **`nibe_local` 1.1.3** — heat-pump diagnostic metrics convert vendor kW/kWh to W/Wh at emit (case and surrounding spaces folded, so `kW ` still converts). Headline names `hp_energy_consumed_kwh` and `hp_energy_produced_kwh` stay so existing series keys do not move; the unit field is Wh. `DRIVER.read_only = true` so the signed artifact matches the observe-only command path. HTTP GET and JSON decode wrap in `pcall`. +- **`myuplink` 1.2.1** — bulk kW/kWh points and the `hp_power_w` headline convert to W/Wh at emit. There are no `hp_energy_*_kwh` headlines; energy, if the pump reports it, is a sanitized bulk name with unit Wh. - **acuvim** 0.4.2, **50-125k-svk** 0.2.3, **50-125k-svk-slew** 0.1.12, **50-125k-svk-ac-slew** 0.2.4, **deye-svk** 0.2.1, **konja-261-svk** 0.3.1 — comments and metadata only: remove references to internal services and diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index fd1e2af..cbb90ca 100644 --- a/SUPPORT_STATUS.md +++ b/SUPPORT_STATUS.md @@ -96,10 +96,10 @@ Catalog source is not proof that a target can install or run a driver. | kstar | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | mennekes | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | mennekes | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| myuplink | 1.2.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| myuplink | 1.2.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| nibe_local | 1.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| nibe_local | 1.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| myuplink | 1.2.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| myuplink | 1.2.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| nibe_local | 1.1.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| nibe_local | 1.1.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | opendtu | 1.0.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | opendtu | 1.0.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | opendtu_mqtt | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | diff --git a/devices.yaml b/devices.yaml index 07c3710..993abb1 100644 --- a/devices.yaml +++ b/devices.yaml @@ -967,7 +967,7 @@ manufacturers: protocols: - protocol: http driver: "myuplink" - version: "1.2.0" + version: "1.2.1" ders: [heatpump] control: false firmware_versions: "" @@ -993,7 +993,7 @@ manufacturers: protocols: - protocol: http driver: "nibe_local" - version: "1.1.2" + version: "1.1.3" ders: [heatpump] control: false firmware_versions: "" diff --git a/drivers/lua/myuplink.lua b/drivers/lua/myuplink.lua index d337d1c..8cccdbd 100644 --- a/drivers/lua/myuplink.lua +++ b/drivers/lua/myuplink.lua @@ -48,7 +48,7 @@ DRIVER = { id = "myuplink", name = "MyUplink Heat Pump (telemetry)", manufacturer = "MyUplink (NIBE, Bosch, Atlantic, Daikin, ...)", - version = "1.2.0", + version = "1.2.1", protocols = { "http" }, capabilities = { "apicreds" }, -- Says what the header, the description and driver_command have always @@ -222,6 +222,30 @@ local function scale_value(raw, unit) return raw end +-- Fold vendor unit strings so "kW " / "KW" still convert. Empty → already SI. +local function fold_unit(unit) + if type(unit) ~= "string" then return "" end + unit = unit:gsub("^%s+", "") + unit = unit:gsub("%s+$", "") + return string.lower(unit) +end + +-- kW → W. Unknown non-empty units keep their vendor string so we do not +-- relabel a missed kilowatt reading as watts. +local function to_watts(value, unit) + local folded = fold_unit(unit) + if folded == "kw" then return value * 1000.0, "W" end + if folded == "w" or folded == "" then return value, "W" end + return value, unit +end + +local function to_wh(value, unit) + local folded = fold_unit(unit) + if folded == "kwh" then return value * 1000.0, "Wh" end + if folded == "wh" or folded == "" then return value, "Wh" end + return value, unit +end + -- Turn a MyUplink parameterName into a stable snake_case metric name, -- prefixed hp_. Non-ASCII and punctuation collapse to single underscores; -- empty names fall back to the parameterId. @@ -323,9 +347,9 @@ function driver_poll() if by_id[PARAM_POWER] then local raw = tonumber(by_id[PARAM_POWER].value) or 0 -- MyUplink points report the unit in "parameterUnit" (not "unit"). - local unit = by_id[PARAM_POWER].parameterUnit or by_id[PARAM_POWER].unit - local power_w = (unit == "kW") and raw * 1000 or raw - host.emit_metric("hp_power_w", power_w, "W") + local unit = by_id[PARAM_POWER].parameterUnit or by_id[PARAM_POWER].unit or "" + local power_w, out_unit = to_watts(raw, unit) + host.emit_metric("hp_power_w", power_w, out_unit) end if by_id[PARAM_HW_TEMP] then host.emit_metric("hp_hw_top_temp_c", decode_temp(by_id[PARAM_HW_TEMP]) or 0, "°C") end if by_id[PARAM_INDOOR_TEMP] then host.emit_metric("hp_indoor_temp_c", decode_temp(by_id[PARAM_INDOOR_TEMP]) or 0, "°C") end @@ -348,7 +372,14 @@ function driver_poll() local name = sanitize_metric_name(pt.parameterName, pid) if seen[name] then name = name .. "_" .. pid end seen[name] = true - host.emit_metric(name, scale_value(raw, unit), unit) + local value = scale_value(raw, unit) + local folded = fold_unit(unit) + if folded == "kw" or folded == "w" then + value, unit = to_watts(value, unit) + elseif folded == "kwh" or folded == "wh" then + value, unit = to_wh(value, unit) + end + host.emit_metric(name, value, unit) end end end diff --git a/drivers/lua/nibe_local.lua b/drivers/lua/nibe_local.lua index fb25464..5dee250 100644 --- a/drivers/lua/nibe_local.lua +++ b/drivers/lua/nibe_local.lua @@ -57,9 +57,12 @@ DRIVER = { id = "nibe-local", name = "NIBE REST API S-series", manufacturer = "NIBE", - version = "1.0.0", + version = "1.1.3", protocols = { "http" }, capabilities = { "apicreds" }, + -- Without this the channel infers control from driver_command and + -- publishes a write-capable artifact. The command path refuses every call. + read_only = true, description = "Read-only NIBE S-series heat-pump telemetry over the on-prem Local REST API (HTTPS + Basic auth, self-signed cert pinned via tls_pin_sha256). Emits compressor/used power, lifetime energy meters, and the full ~980-point register map. Observe-only — no control.", homepage = "https://www.nibe.eu", authors = { "HuggeK", "FTW contributors" }, @@ -94,20 +97,21 @@ local last_emitted = {} -- The BULK of telemetry is metadata-driven (every point self-describes its -- unit + divisor), so reading any S-series pump needs NO per-model code. The -- only model-specific knobs are the handful of STABLE headline aliases --- (hp_power_w, hp_outdoor_temp_c, …) that web/heating.js + the thermal twin --- read by fixed name. Each maps to a local-API variableId, resolved per pump +-- (hp_power_w, hp_outdoor_temp_c, …) that hosts read by fixed name. Each +-- maps to a local-API variableId, resolved per pump -- in priority order: explicit config override > model profile > generic -- S-series default. --- Logical headline -> { config override key, emitted metric name, watts? }. +-- Logical headline -> { config override key, emitted metric name, watts?, wh? }. local HEADLINES = { { key = "power", cfg = "param_power_id", name = "hp_power_w", watts = true }, { key = "used", cfg = "param_used_id", name = "hp_used_power_w", watts = true }, { key = "hw", cfg = "param_hw_temp_id", name = "hp_hw_top_temp_c" }, { key = "indoor", cfg = "param_indoor_temp_id", name = "hp_indoor_temp_c" }, { key = "outdoor", cfg = "param_outdoor_temp_id", name = "hp_outdoor_temp_c" }, - { key = "econs", cfg = "param_energy_consumed_id", name = "hp_energy_consumed_kwh" }, - { key = "eprod", cfg = "param_energy_produced_id", name = "hp_energy_produced_kwh" }, + -- Name stays _kwh so existing series keys do not move. Unit at emit is Wh. + { key = "econs", cfg = "param_energy_consumed_id", name = "hp_energy_consumed_kwh", wh = true }, + { key = "eprod", cfg = "param_energy_produced_id", name = "hp_energy_produced_kwh", wh = true }, { key = "dm", cfg = "param_degree_minutes_id", name = "hp_degree_minutes" }, } @@ -179,11 +183,28 @@ local function sanitize_metric_name(title, id) return "hp_" .. s end --- Watts normalisation for the power headline metrics: some models report --- compressor power in kW, others in W. Emit W either way. +-- Fold vendor unit strings so "kW " / "KW" still convert. Empty → already SI. +local function fold_unit(unit) + if type(unit) ~= "string" then return "" end + unit = unit:gsub("^%s+", "") + unit = unit:gsub("%s+$", "") + return string.lower(unit) +end + +-- kW → W. Unknown non-empty units keep their vendor string so we do not +-- relabel a missed kilowatt reading as watts. local function to_watts(value, unit) - if unit == "kW" then return value * 1000.0, "W" end - return value, (unit ~= "" and unit or "W") + local folded = fold_unit(unit) + if folded == "kw" then return value * 1000.0, "W" end + if folded == "w" or folded == "" then return value, "W" end + return value, unit +end + +local function to_wh(value, unit) + local folded = fold_unit(unit) + if folded == "kwh" then return value * 1000.0, "Wh" end + if folded == "wh" or folded == "" then return value, "Wh" end + return value, unit end -- The NIBE Modbus register id for a point (metadata.modbusRegisterID), formatted @@ -222,7 +243,7 @@ local function build_canon(profile, config) CANON = {} for _, h in ipairs(HEADLINES) do local id = s(config[h.cfg]) or s(profile[h.key]) or s(PROFILES.default[h.key]) - if id then CANON[id] = { name = h.name, watts = h.watts } end + if id then CANON[id] = { name = h.name, watts = h.watts, wh = h.wh } end end end @@ -248,10 +269,12 @@ local function auth_headers() end local function api_get(path) - local resp, err = host.http_get(base_url .. path, auth_headers()) + local get_ok, resp, err = pcall(host.http_get, base_url .. path, auth_headers()) + if not get_ok then return nil, tostring(resp) end if err then return nil, tostring(err) end - local data = host.json_decode(resp) - if not data then return nil, "json decode failed" end + local decode_ok, data, derr = pcall(host.json_decode, resp) + if not decode_ok then return nil, tostring(data) end + if not data then return nil, tostring(derr or "json decode failed") end return data, nil end @@ -390,7 +413,12 @@ function driver_poll() name = name .. "_" .. tostring(id) end local value = scaled - if canon and canon.watts then value, unit = to_watts(scaled, unit) end + local folded = fold_unit(unit) + if folded == "kw" or folded == "w" or (canon and canon.watts) then + value, unit = to_watts(scaled, unit) + elseif folded == "kwh" or folded == "wh" or (canon and canon.wh) then + value, unit = to_wh(scaled, unit) + end -- Stable headline series retain one-minute resolution. The bulk -- map records transitions plus an hourly complete snapshot. diff --git a/drivers/tests/lua_harness/host_mock.lua b/drivers/tests/lua_harness/host_mock.lua index 8c08564..34c3d2f 100644 --- a/drivers/tests/lua_harness/host_mock.lua +++ b/drivers/tests/lua_harness/host_mock.lua @@ -351,6 +351,29 @@ function host.http_get(url) error("http_get: no mock response for URL: " .. tostring(url)) end +function host.http_post(url, body, headers) + record_call("http_post", url, body, headers) + local resp = host._http_responses[url] + if resp then + return resp + end + for pattern_url, posted in pairs(host._http_responses) do + if string.find(url, pattern_url, 1, true) then + return posted + end + end + error("http_post: no mock response for URL: " .. tostring(url)) +end + +function host.set_poll_interval(interval_ms) + record_call("set_poll_interval", interval_ms) +end + +function host.persist_secret(key, value) + record_call("persist_secret", key, value) + return true +end + --------------------------------------------------------------------------- -- Serial functions --------------------------------------------------------------------------- diff --git a/drivers/tests/lua_harness/test_hp_si_units.lua b/drivers/tests/lua_harness/test_hp_si_units.lua new file mode 100644 index 0000000..99519a4 --- /dev/null +++ b/drivers/tests/lua_harness/test_hp_si_units.lua @@ -0,0 +1,178 @@ +-- Pin kW/kWh → W/Wh at emit for nibe_local and myuplink. +-- +-- Usage: lua55 test_hp_si_units.lua + +local script_dir = arg[0]:match("(.*/)") or "./" +dofile(script_dir .. "host_mock.lua") + +local ROOT = script_dir .. "../../../" +local failed = 0 + +local function fail(msg) + failed = failed + 1 + io.stderr:write("FAIL " .. msg .. "\n") +end + +local function near(a, b) + return type(a) == "number" and math.abs(a - b) < 0.01 +end + +local function expect_metric(name, value, unit) + local m = host._metrics[name] + if not m then + fail(name .. ": not emitted") + return + end + if not near(m.value, value) then + fail(name .. ": value " .. tostring(m.value) .. " want " .. tostring(value)) + end + if m.unit ~= unit then + fail(name .. ": unit " .. tostring(m.unit) .. " want " .. tostring(unit)) + end +end + +--------------------------------------------------------------------------- +-- nibe_local: divisor then SI conversion, folded units, unexpected preserved +--------------------------------------------------------------------------- + +host.reset() +dofile(ROOT .. "drivers/lua/nibe_local.lua") + +host._http_responses["https://192.168.1.180:8443/api/v1/devices/SN1/points"] = [[{ + "1801": { + "title": "Compressor", + "value": {"integerValue": 15}, + "metadata": {"unit": "kW ", "divisor": 10, "variableSize": "s16", "modbusRegisterID": 1} + }, + "22130": { + "title": "Used", + "value": {"integerValue": 800}, + "metadata": {"unit": "W", "divisor": 1, "variableSize": "s16", "modbusRegisterID": 2} + }, + "28393": { + "title": "Consumed", + "value": {"integerValue": 53999}, + "metadata": {"unit": "KWh", "divisor": 10, "variableSize": "s32", "modbusRegisterID": 3} + }, + "28392": { + "title": "Produced", + "value": {"integerValue": 42}, + "metadata": {"divisor": 1, "variableSize": "s32", "modbusRegisterID": 4} + }, + "4001": { + "title": "Bulk kilowatt", + "value": {"integerValue": 25}, + "metadata": {"unit": "kW", "divisor": 10, "variableSize": "s16", "modbusRegisterID": 5} + }, + "4002": { + "title": "Bulk watts", + "value": {"integerValue": 1200}, + "metadata": {"unit": "W", "divisor": 1, "variableSize": "s16", "modbusRegisterID": 6} + }, + "4003": { + "title": "Bulk energy", + "value": {"integerValue": 12}, + "metadata": {"unit": "kWh", "divisor": 1, "variableSize": "s32", "modbusRegisterID": 7} + }, + "4004": { + "title": "Speed", + "value": {"integerValue": 3000}, + "metadata": {"unit": "rpm", "divisor": 1, "variableSize": "s16", "modbusRegisterID": 8} + }, + "4005": { + "title": "Odd power", + "value": {"integerValue": 9}, + "metadata": {"unit": "GM", "divisor": 1, "variableSize": "s16", "modbusRegisterID": 9} + } +}]] + +driver_init({ + host = "192.168.1.180", + username = "user", + password = "pass", + device_id = "SN1", +}) +local nibe_ok, nibe_interval = pcall(driver_poll) +if not nibe_ok then + fail("nibe poll threw: " .. tostring(nibe_interval)) +else + expect_metric("hp_power_w", 1500, "W") + expect_metric("hp_used_power_w", 800, "W") + expect_metric("hp_energy_consumed_kwh", 5399900, "Wh") + expect_metric("hp_energy_produced_kwh", 42, "Wh") + expect_metric("hp_bulk_kilowatt", 2500, "W") + expect_metric("hp_bulk_watts", 1200, "W") + expect_metric("hp_bulk_energy", 12000, "Wh") + expect_metric("hp_speed", 3000, "rpm") + expect_metric("hp_odd_power", 9, "GM") + local consumed = host._metrics["hp_energy_consumed_kwh"] + if consumed and consumed.unit == "Wh" and consumed.name then + fail("headline name must stay hp_energy_consumed_kwh") + end +end + +-- Missing points URL must take the retry path, not abort the poll. +driver_cleanup() +host.reset() +driver_init({ + host = "192.168.1.180", + username = "user", + password = "pass", + device_id = "SN1", +}) +local err_ok, err_ret = pcall(driver_poll) +if not err_ok then + fail("nibe poll without HTTP fixture threw: " .. tostring(err_ret)) +elseif type(err_ret) ~= "number" then + fail("nibe poll without HTTP fixture returned " .. tostring(err_ret)) +end + +--------------------------------------------------------------------------- +-- myuplink: headline kW, bulk kWh, folded "kW ", unexpected preserved +--------------------------------------------------------------------------- + +driver_cleanup() +host.reset() +dofile(ROOT .. "drivers/lua/myuplink.lua") + +host._http_responses["https://api.myuplink.com/oauth/token"] = + [[{"access_token":"t","expires_in":3600}]] +host._http_responses["https://api.myuplink.com/v2/devices/DEV1/points"] = [[ +[ + {"parameterId": 10012, "value": 1.5, "parameterUnit": "kW", "parameterName": "Compressor"}, + {"parameterId": 40013, "value": 48, "parameterUnit": "°C", "parameterName": "HW"}, + {"parameterId": 40033, "value": 21, "parameterUnit": "°C", "parameterName": "Indoor"}, + {"parameterId": 40004, "value": 5, "parameterUnit": "°C", "parameterName": "Outdoor"}, + {"parameterId": 99901, "value": 12.3, "parameterUnit": "kWh", "parameterName": "Energy consumed"}, + {"parameterId": 99902, "value": 800, "parameterUnit": "W", "parameterName": "Used power"}, + {"parameterId": 99903, "value": 2.0, "parameterUnit": "kW ", "parameterName": "Odd kilowatt"}, + {"parameterId": 99904, "value": 3000, "parameterUnit": "rpm", "parameterName": "Speed"} +] +]] + +driver_init({ + client_id = "id", + client_secret = "secret", + refresh_token = "refresh", + device_id = "DEV1", + setup_retry_ms = 0, +}) +local up_ok, up_interval = pcall(driver_poll) +if not up_ok then + fail("myuplink poll threw: " .. tostring(up_interval)) +else + expect_metric("hp_power_w", 1500, "W") + expect_metric("hp_energy_consumed", 12300, "Wh") + expect_metric("hp_used_power", 800, "W") + expect_metric("hp_odd_kilowatt", 2000, "W") + expect_metric("hp_speed", 3000, "rpm") + if host._metrics["hp_energy_consumed_kwh"] then + fail("myuplink must not emit hp_energy_consumed_kwh") + end +end + +if failed > 0 then + io.stderr:write(failed .. " checks failed\n") + os.exit(1) +end +print("PASS") diff --git a/drivers/tests/test_driver_contract.py b/drivers/tests/test_driver_contract.py index d00e513..ee1132e 100644 --- a/drivers/tests/test_driver_contract.py +++ b/drivers/tests/test_driver_contract.py @@ -65,7 +65,7 @@ def test_driver_poll_returns_interval(self, driver_name): ) if poll_match: body = poll_match.group(1) - assert re.search(r'return\s+(?:\d+|[a-z_]\w*(?:\([^)]*\))?)', body), \ + assert re.search(r'return\s+(?:\d+|[A-Za-z_]\w*(?:\([^)]*\))?)', body), \ f"{driver_name}: driver_poll should return a poll interval" def test_calls_set_make_in_init(self, driver_name): diff --git a/drivers/tests/test_hp_si_units.py b/drivers/tests/test_hp_si_units.py new file mode 100644 index 0000000..e02b6cf --- /dev/null +++ b/drivers/tests/test_hp_si_units.py @@ -0,0 +1,27 @@ +"""nibe_local and myuplink convert vendor kW/kWh to W/Wh at emit.""" + +from pathlib import Path +import subprocess + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +LUA = ROOT / "lua55" + +pytestmark = pytest.mark.skipif( + not LUA.exists(), reason="run make check to build ./lua55") + + +def test_nibe_local_myuplink_si_units_at_emit(): + result = subprocess.run( + [ + str(LUA), + "drivers/tests/lua_harness/test_hp_si_units.lua", + ], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert "PASS" in result.stdout diff --git a/drivers/tests/test_http_drivers.py b/drivers/tests/test_http_drivers.py index da9975d..3dfecad 100644 --- a/drivers/tests/test_http_drivers.py +++ b/drivers/tests/test_http_drivers.py @@ -35,17 +35,14 @@ def test_constructs_base_url(self, driver_name): code = read_driver(driver_name) clean = strip_lua_comments(code) - # Look for URL construction pattern: - # base_url = "http://" .. config.host .. ":" .. port - # or similar patterns - has_url_construction = ( - re.search(r'"http://".*config\.host', clean) - or re.search(r'config\.host.*"http://"', clean) - or re.search(r'base_url\s*=\s*"http://"', clean) - ) - - assert has_url_construction, ( - f"{driver_name}: HTTP driver should construct URL from config.host" + # Local HTTP builds the URL from config.host. The scheme may be + # http:// or https:// (a LAN device that only speaks TLS, with a pin). + has_host = "config.host" in clean or 'config["host"]' in clean + has_scheme = '"http://"' in clean or '"https://"' in clean + + assert has_host and has_scheme, ( + f"{driver_name}: HTTP driver should construct an http(s) URL " + f"from config.host" ) def test_uses_json_decode(self, driver_name): @@ -96,15 +93,14 @@ class TestHttpUrlSafety: """Validate URL construction safety for local HTTP drivers.""" def test_uses_http_scheme(self, driver_name): - """Local HTTP drivers should use http://, not HTTPS.""" + """Local HTTP drivers use http://, or https:// when the device pins TLS.""" skip_if_cloud(driver_name) code = read_driver(driver_name) clean = strip_lua_comments(code) - # Remove comments for checking - # Should have http:// somewhere in URL construction - assert '"http://"' in clean, ( - f"{driver_name}: HTTP driver should use 'http://' scheme" + assert '"http://"' in clean or '"https://"' in clean, ( + f"{driver_name}: HTTP driver should use an 'http://' or " + f"'https://' scheme" ) def test_uses_config_port(self, driver_name): @@ -139,7 +135,7 @@ def test_poll_returns_on_nil_data(self, driver_name): # may expose the request error as a separate return value. has_early_return = bool(re.search( r'if\s+(?:not\s+)?\w+[^\n]*then.*?return\s+' - r'(?:\d+|[a-z_]\w*(?:\([^)]*\))?)', + r'(?:\d+|[A-Za-z_]\w*(?:\([^)]*\))?)', poll_body, re.DOTALL, )) diff --git a/index.yaml b/index.yaml index e187a9f..244d10c 100644 --- a/index.yaml +++ b/index.yaml @@ -423,25 +423,25 @@ drivers: size_bytes: 3766 sha256: "5f985b8917aea7b08fba13c02506b45e40232a763393dc6af7c6eeb6ab5853af" - name: "myuplink" - version: "1.2.0" + version: "1.2.1" tier: core protocol: http connectivity: cloud setup: [vendor_portal] ders: [heatpump] control: false - size_bytes: 16386 - sha256: "6074b70eb2bbe49481fcc59551d65474cb8129ae5370e9c1844c23b08a5f6fd6" + size_bytes: 17549 + sha256: "6ac47f27388ba8c89e5aafc1b764f598a0fe8cb89e12bcabdae855419f8d5e42" - name: "nibe_local" - version: "1.1.2" + version: "1.1.3" tier: core protocol: http connectivity: local setup: [device_screen] ders: [heatpump] control: false - size_bytes: 18227 - sha256: "b89177004a2eff5a5b60e2f275f584f079acf02876c049b231ba3170bb35ac50" + size_bytes: 19446 + sha256: "7f020f70eb4109c58f1af1097e37597280447fcbc95246b30e9638413592039e" - name: "opendtu" version: "1.0.2" tier: community diff --git a/manifests/myuplink.yaml b/manifests/myuplink.yaml index ffc3bd2..972f368 100644 --- a/manifests/myuplink.yaml +++ b/manifests/myuplink.yaml @@ -1,5 +1,5 @@ name: "myuplink" -version: "1.2.0" +version: "1.2.1" tier: core author: "Sourceful Labs AB" protocol: http @@ -21,9 +21,9 @@ upstream_docs: kind: changelog url_stability: stable min_host_version: "2.0.0" -size_bytes: 16386 +size_bytes: 17549 dkb_id: "myuplink" -sha256: "6074b70eb2bbe49481fcc59551d65474cb8129ae5370e9c1844c23b08a5f6fd6" +sha256: "6ac47f27388ba8c89e5aafc1b764f598a0fe8cb89e12bcabdae855419f8d5e42" signature: "" bytecode_sha256: "" diff --git a/manifests/nibe_local.yaml b/manifests/nibe_local.yaml index 46c2720..edc23a8 100644 --- a/manifests/nibe_local.yaml +++ b/manifests/nibe_local.yaml @@ -1,5 +1,5 @@ name: "nibe_local" -version: "1.1.2" +version: "1.1.3" tier: core author: "HuggeK with the help of Claude Code" protocol: http @@ -21,9 +21,9 @@ upstream_docs: kind: changelog url_stability: stable min_host_version: "2.0.0" -size_bytes: 18227 +size_bytes: 19446 dkb_id: "nibe_local" -sha256: "b89177004a2eff5a5b60e2f275f584f079acf02876c049b231ba3170bb35ac50" +sha256: "7f020f70eb4109c58f1af1097e37597280447fcbc95246b30e9638413592039e" signature: "" bytecode_sha256: "" diff --git a/support-status.json b/support-status.json index 39e8c21..7ca720f 100644 --- a/support-status.json +++ b/support-status.json @@ -1290,7 +1290,7 @@ }, { "catalog_source": true, - "catalog_version": "1.2.0", + "catalog_version": "1.2.1", "driver_id": "myuplink", "package_id": null, "targets": { @@ -1318,7 +1318,7 @@ }, { "catalog_source": true, - "catalog_version": "1.1.2", + "catalog_version": "1.1.3", "driver_id": "nibe_local", "package_id": null, "targets": { diff --git a/tests/test_ftw_repository.py b/tests/test_ftw_repository.py index 2800637..9199915 100644 --- a/tests/test_ftw_repository.py +++ b/tests/test_ftw_repository.py @@ -179,7 +179,7 @@ def test_publication_contains_the_full_read_only_catalog( assert {"sungrow", "pixii", "alphaess", "ferroamp"} <= controlling # And a driver that declares read_only in its own DRIVER table stays a # meter, whatever anything else says. - assert not ({"sdm630", "zap", "esphome_dsmr"} & controlling) + assert not ({"sdm630", "zap", "esphome_dsmr", "nibe_local", "myuplink"} & controlling) assert all(driver["host_api"] == {"min": 1, "max": 1} for driver in manifest["drivers"]) assert all(driver["metadata"]["source"] == "upstream" for driver in manifest["drivers"]) assert all(driver["source_commit"] == COMMIT for driver in manifest["drivers"])