Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## myuplink 1.2.4

Improve NIBE F750 telemetry from hardware-observed MyUplink parameter data.
Parameter 40013 is documented as BT7 hot-water top temperature, and parameter
40940 is exposed as the canonical hp_degree_minutes reading. Ignore the NIBE
-32768 unavailable-value sentinel instead of emitting bogus telemetry, and
remove embedded Unicode soft hyphens from generated metric names. The driver
remains read-only. No F750 electrical compressor-power mapping is claimed.


## heishamon 0.8.0

Read `hp_power_w` from `main/Heat_Power_Consumption` (TOP16), with a
Expand Down
4 changes: 2 additions & 2 deletions SUPPORT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ 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 |
| mennekes | 1.0.3 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| mennekes | 1.0.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| myuplink | 1.2.3 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| myuplink | 1.2.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| myuplink | 1.2.4 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| myuplink | 1.2.4 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| nibe_local | 1.2.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| nibe_local | 1.2.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| opendtu | 1.0.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
Expand Down
2 changes: 1 addition & 1 deletion devices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ manufacturers:
protocols:
- protocol: http
driver: "myuplink"
version: "1.2.3"
version: "1.2.4"
ders: [heatpump]
control: false
firmware_versions: ""
Expand Down
45 changes: 34 additions & 11 deletions drivers/lua/myuplink.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ DRIVER = {
id = "myuplink",
name = "MyUplink Heat Pump (telemetry)",
manufacturer = "MyUplink (NIBE, Bosch, Atlantic, Daikin, ...)",
version = "1.2.3",
version = "1.2.4",
protocols = { "http" },
capabilities = { "apicreds" },
-- Says what the header, the description and driver_command have always
Expand Down Expand Up @@ -90,9 +90,10 @@ local device_id = nil

-- Parameter IDs (NIBE defaults, overridable via config)
local PARAM_POWER = "10012" -- compressor power (W)
local PARAM_HW_TEMP = "40013" -- BT6 hot water top temp
local PARAM_INDOOR_TEMP = "40033" -- BT50 room temperature
local PARAM_OUTDOOR_TEMP = "40004" -- BT1 outdoor temperature
local PARAM_HW_TEMP = "40013" -- BT7 hot water top temperature
local PARAM_INDOOR_TEMP = "40033" -- BT50 room temperature
local PARAM_OUTDOOR_TEMP = "40004" -- BT1 outdoor temperature
local PARAM_DEGREE_MINUTES = "40940" -- F750 current degree-minute value

-- ---- Helpers -------------------------------------------------------------

Expand Down Expand Up @@ -206,10 +207,16 @@ local function fetch_all_points()
return data, by_id, nil
end

local function valid_raw(raw)
raw = tonumber(raw)
if raw == nil or raw == -32768 then return nil end
return raw
end

local function decode_temp(pt)
if not pt then return nil end
local raw = tonumber(pt.value)
if not raw then return nil end
local raw = valid_raw(pt.value)
if raw == nil then return nil end
if math.abs(raw) > 100 then return raw / 10 end -- NIBE °C×10 encoding
return raw
end
Expand Down Expand Up @@ -251,6 +258,10 @@ end
-- empty names fall back to the parameterId.
local function sanitize_metric_name(name, pid)
local s = string.lower(name or "")
-- MyUplink/NIBE parameter names sometimes contain U+00AD SOFT HYPHEN
-- inside ordinary words. Remove its UTF-8 bytes instead of turning it
-- into an underscore (for example "com­pressor" -> "compressor").
s = string.gsub(s, string.char(194, 173), "")
s = string.gsub(s, "[^a-z0-9]+", "_")
s = string.gsub(s, "^_+", "")
s = string.gsub(s, "_+$", "")
Expand Down Expand Up @@ -301,7 +312,8 @@ function driver_init(config)
PARAM_POWER = ov("param_power_id", PARAM_POWER)
PARAM_HW_TEMP = ov("param_hw_temp_id", PARAM_HW_TEMP)
PARAM_INDOOR_TEMP = ov("param_indoor_temp_id", PARAM_INDOOR_TEMP)
PARAM_OUTDOOR_TEMP = ov("param_outdoor_temp_id", PARAM_OUTDOOR_TEMP)
PARAM_OUTDOOR_TEMP = ov("param_outdoor_temp_id", PARAM_OUTDOOR_TEMP)
PARAM_DEGREE_MINUTES = ov("param_degree_minutes_id", PARAM_DEGREE_MINUTES)
-- base_url override exists for tests; production uses api.myuplink.com.
if config.base_url and config.base_url ~= "" then BASE_URL = config.base_url end
-- setup_retry_ms override exists for tests (0 = retry immediately).
Expand Down Expand Up @@ -351,22 +363,33 @@ function driver_poll()
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
if by_id[PARAM_OUTDOOR_TEMP] then host.emit_metric("hp_outdoor_temp_c", decode_temp(by_id[PARAM_OUTDOOR_TEMP]) or 0, "°C") end
local hw_temp = decode_temp(by_id[PARAM_HW_TEMP])
if hw_temp ~= nil then host.emit_metric("hp_hw_top_temp_c", hw_temp, "°C") end

local indoor_temp = decode_temp(by_id[PARAM_INDOOR_TEMP])
if indoor_temp ~= nil then host.emit_metric("hp_indoor_temp_c", indoor_temp, "°C") end

local outdoor_temp = decode_temp(by_id[PARAM_OUTDOOR_TEMP])
if outdoor_temp ~= nil then host.emit_metric("hp_outdoor_temp_c", outdoor_temp, "°C") end

if by_id[PARAM_DEGREE_MINUTES] then
local dm = valid_raw(by_id[PARAM_DEGREE_MINUTES].value)
if dm ~= nil then host.emit_metric("hp_degree_minutes", dm, "DM") end
end

-- Everything else → hp_<sanitized name> with its unit, so the UI can
-- auto-group (temperatures / power / frequency / state / …). Skip the
-- four canonical parameter IDs so they aren't emitted twice.
local canonical = {
[tostring(PARAM_POWER)] = true, [tostring(PARAM_HW_TEMP)] = true,
[tostring(PARAM_INDOOR_TEMP)] = true, [tostring(PARAM_OUTDOOR_TEMP)] = true,
[tostring(PARAM_DEGREE_MINUTES)] = true,
}
local seen = {}
for _, pt in ipairs(data) do
local pid = tostring(pt.parameterId or "")
if pt.parameterId and not canonical[pid] then
local raw = tonumber(pt.value)
local raw = valid_raw(pt.value)
if raw ~= nil then
local unit = pt.parameterUnit or pt.unit or ""
local name = sanitize_metric_name(pt.parameterName, pid)
Expand Down
86 changes: 86 additions & 0 deletions drivers/tests/lua_harness/test_myuplink_f750.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
local ROOT = "./"

dofile(ROOT .. "drivers/tests/lua_harness/host_mock.lua")

local failed = 0

local function fail(msg)
io.stderr:write("FAIL: " .. msg .. "\n")
failed = failed + 1
end

local function expect_metric(name, value, unit)
local metric = host._metrics[name]
if not metric then
fail("missing metric " .. name)
return
end
if metric.value ~= value then
fail(name .. ": expected value " .. tostring(value)
.. ", got " .. tostring(metric.value))
end
if metric.unit ~= unit then
fail(name .. ": expected unit " .. tostring(unit)
.. ", got " .. tostring(metric.unit))
end
end

local function expect_absent(name)
if host._metrics[name] then
fail("unexpected metric " .. name)
end
end

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/F750/points"] = [[
[
{"parameterId":40004,"value":11.6,"parameterUnit":"°C","parameterName":"Outdoor temperature BT1"},
{"parameterId":40013,"value":50.3,"parameterUnit":"°C","parameterName":"Hot water top BT7"},
{"parameterId":40014,"value":49.1,"parameterUnit":"°C","parameterName":"Hot water charging BT6"},
{"parameterId":40033,"value":22.8,"parameterUnit":"°C","parameterName":"Room temperature BT50"},
{"parameterId":40940,"value":86,"parameterUnit":"DM","parameterName":"current value"},
{"parameterId":41778,"value":0,"parameterUnit":"Hz","parameterName":"Current com­pressor fre­quency"},
{"parameterId":43084,"value":0,"parameterUnit":"kW","parameterName":"Power internal add. heat"},
{"parameterId":43124,"value":-32768,"parameterUnit":"m3/h","parameterName":"Reference air speed sensor"},
{"parameterId":44298,"value":10494.7,"parameterUnit":"kWh","parameterName":"Hot water incl internal add heat"},
{"parameterId":50225,"value":-32768,"parameterUnit":"°C","parameterName":"Current temperature system 1"},
{"parameterId":50233,"value":-32768,"parameterUnit":"°C","parameterName":"Set point temp system 1 heat"}
]
]]

driver_init({
client_id = "id",
client_secret = "secret",
refresh_token = "refresh",
device_id = "F750",
setup_retry_ms = 0,
})

local ok, interval = pcall(driver_poll)
if not ok then
fail("myuplink poll threw: " .. tostring(interval))
else
expect_metric("hp_hw_top_temp_c", 50.3, "°C")
expect_metric("hp_indoor_temp_c", 22.8, "°C")
expect_metric("hp_outdoor_temp_c", 11.6, "°C")

expect_metric("hp_degree_minutes", 86, "DM")
expect_metric("hp_current_compressor_frequency", 0, "Hz")
expect_metric("hp_power_internal_add_heat", 0, "W")
expect_metric("hp_hot_water_incl_internal_add_heat", 10494700, "Wh")

expect_absent("hp_reference_air_speed_sensor")
expect_absent("hp_current_temperature_system_1")
expect_absent("hp_set_point_temp_system_1_heat")
end

if failed > 0 then
io.stderr:write(failed .. " checks failed\n")
os.exit(1)
end

print("PASS")
22 changes: 22 additions & 0 deletions drivers/tests/test_myuplink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""MyUplink driver regression tests for NIBE F750 telemetry."""

from __future__ import annotations

import subprocess
from pathlib import Path

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_myuplink_f750_telemetry():
result = subprocess.run(
[str(LUA), "drivers/tests/lua_harness/test_myuplink_f750.lua"],
capture_output=True, text=True, cwd=ROOT)
assert result.returncode == 0, result.stdout + result.stderr
assert "PASS" in result.stdout
6 changes: 3 additions & 3 deletions index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -413,15 +413,15 @@ drivers:
size_bytes: 3766
sha256: "5f985b8917aea7b08fba13c02506b45e40232a763393dc6af7c6eeb6ab5853af"
- name: "myuplink"
version: "1.2.3"
version: "1.2.4"
tier: core
protocol: http
connectivity: cloud
setup: [vendor_portal]
ders: [heatpump]
control: false
size_bytes: 17562
sha256: "1eca0cdd1cd2dabf772cba90b359d55e7c8f6e350e403623227e9a5a03b20d9c"
size_bytes: 18431
sha256: "5667359d514403650634d6a2ca120a99d391dce2a01d63bf7e45a64a3f6054a3"
- name: "nibe_local"
version: "1.2.1"
tier: core
Expand Down
6 changes: 3 additions & 3 deletions manifests/myuplink.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "myuplink"
version: "1.2.3"
version: "1.2.4"
tier: core
author: "Sourceful Labs AB"
protocol: http
Expand All @@ -21,9 +21,9 @@ upstream_docs:
kind: changelog
url_stability: stable
min_host_version: "2.0.0"
size_bytes: 17562
size_bytes: 18431
dkb_id: "myuplink"
sha256: "1eca0cdd1cd2dabf772cba90b359d55e7c8f6e350e403623227e9a5a03b20d9c"
sha256: "5667359d514403650634d6a2ca120a99d391dce2a01d63bf7e45a64a3f6054a3"
signature: ""

bytecode_sha256: ""
2 changes: 1 addition & 1 deletion support-status.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,7 @@
},
{
"catalog_source": true,
"catalog_version": "1.2.3",
"catalog_version": "1.2.4",
"driver_id": "myuplink",
"targets": {
"blixt-l1": {
Expand Down
Loading