Skip to content
Merged
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

## [Unreleased]

### pixii_pv 0.3.3

Report zero PV generation when the measured phase sum is negative. Negating
that sum used to emit positive PV power, which FTW rejects, so fresh readings
could leave PV stale or missing after sunset. Sum the phases before limiting
generation to zero; keep their signed diagnostic values. Missing, invalid or
non-finite phase power emits no PV reading. Idle polls do not refresh PV.

## pixii 2.1.9

A power cut left the driver offline until someone restarted it. Each register
Expand Down
4 changes: 2 additions & 2 deletions SUPPORT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ Catalog source is not proof that a target can install or run a driver.
| p1_meter | 2.0.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| pixii | 2.1.9 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| pixii | 2.1.9 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.3 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| remne_p1ib | 0.1.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| remne_p1ib | 0.1.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| saj | 1.2.0 | 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 @@ -1021,7 +1021,7 @@ manufacturers:
protocols:
- protocol: mqtt
driver: "pixii_pv"
version: "0.3.2"
version: "0.3.3"
ders: [pv, meter]
control: false
firmware_versions: ""
Expand Down
48 changes: 26 additions & 22 deletions drivers/lua/pixii_pv.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ DRIVER = {
id = "pixii_pv",
name = "Pixii PowerShaper (PV + meter)",
manufacturer = "Pixii",
version = "0.3.2",
version = "0.3.3",
protocols = { "mqtt" },
capabilities = { "pv", "meter" },
read_only = true,
Expand Down Expand Up @@ -108,29 +108,33 @@ function driver_poll()

--------------------------------------------------------------------------
-- PV (external-CT meter on the PV feed).
-- Pixii reports PV as positive generation; site convention requires
-- negative (power leaving the grid boundary into the loads), so negate.
-- Pixii reports generation as positive. A negative net reading means
-- no generation, so report zero PV and keep signed phase diagnostics.
--------------------------------------------------------------------------
if pv_payload then
local w1 = tonumber(pv_payload.meter_w1) or 0
local w2 = tonumber(pv_payload.meter_w2) or 0
local w3 = tonumber(pv_payload.meter_w3) or 0
local pv = {}
pv.w = -(w1 + w2 + w3)
local kwh_imp = tonumber(pv_payload.meter_kwh_imp)
if kwh_imp then pv.generation_wh = kwh_imp * 1000.0 end
host.emit("pv", pv)

host.emit_metric("pv_w_total", pv.w)
host.emit_metric("pv_l1_w", -w1)
host.emit_metric("pv_l2_w", -w2)
host.emit_metric("pv_l3_w", -w3)
if pv_payload.meter_v1 then host.emit_metric("pv_l1_v", tonumber(pv_payload.meter_v1)) end
if pv_payload.meter_v2 then host.emit_metric("pv_l2_v", tonumber(pv_payload.meter_v2)) end
if pv_payload.meter_v3 then host.emit_metric("pv_l3_v", tonumber(pv_payload.meter_v3)) end
if pv_payload.meter_a1 then host.emit_metric("pv_l1_a", tonumber(pv_payload.meter_a1)) end
if pv_payload.meter_a2 then host.emit_metric("pv_l2_a", tonumber(pv_payload.meter_a2)) end
if pv_payload.meter_a3 then host.emit_metric("pv_l3_a", tonumber(pv_payload.meter_a3)) end
local w1 = tonumber(pv_payload.meter_w1)
local w2 = tonumber(pv_payload.meter_w2)
local w3 = tonumber(pv_payload.meter_w3)
local total = w1 and w2 and w3 and (w1 + w2 + w3)
-- Check before clamping: missing or non-finite power is not zero.
if total and total == total and total ~= math.huge and total ~= -math.huge then
local pv = {}
pv.w = -math.max(0, total)
local kwh_imp = tonumber(pv_payload.meter_kwh_imp)
if kwh_imp then pv.generation_wh = kwh_imp * 1000.0 end
host.emit("pv", pv)

host.emit_metric("pv_w_total", pv.w)
host.emit_metric("pv_l1_w", -w1)
host.emit_metric("pv_l2_w", -w2)
host.emit_metric("pv_l3_w", -w3)
if pv_payload.meter_v1 then host.emit_metric("pv_l1_v", tonumber(pv_payload.meter_v1)) end
if pv_payload.meter_v2 then host.emit_metric("pv_l2_v", tonumber(pv_payload.meter_v2)) end
if pv_payload.meter_v3 then host.emit_metric("pv_l3_v", tonumber(pv_payload.meter_v3)) end
if pv_payload.meter_a1 then host.emit_metric("pv_l1_a", tonumber(pv_payload.meter_a1)) end
if pv_payload.meter_a2 then host.emit_metric("pv_l2_a", tonumber(pv_payload.meter_a2)) end
if pv_payload.meter_a3 then host.emit_metric("pv_l3_a", tonumber(pv_payload.meter_a3)) end
end
end

--------------------------------------------------------------------------
Expand Down
104 changes: 104 additions & 0 deletions drivers/tests/test_pixii_pv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Pixii's PV feed stays valid when the measured total falls below zero."""

import json
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 test-driver ID=pixii_pv")


def run_lua(body: str) -> None:
script = '''
package.path = "drivers/tests/lua_harness/?.lua;" .. package.path
require("host_mock")
host.reset()
dofile("drivers/lua/pixii_pv.lua")
driver_init({})

local function poll(payload)
host._mqtt_buffer = {{
topic = "pixii/status/test-pixii/meter_ext6",
payload = payload,
}}
assert(driver_poll() == 1000)
end
''' + body
result = subprocess.run(
[str(LUA), "-e", script], cwd=ROOT, text=True, capture_output=True)
assert result.returncode == 0, result.stdout + result.stderr


@pytest.mark.parametrize(
("phases", "expected"),
[
((1500, 1000, 500), -3000),
((0, 0, 0), 0),
((-1, -2, -3), 0),
((50, -3, -2), -45),
((-50, 3, 2), 0),
((5, -3, -2), 0),
],
)
def test_pixii_pv_reports_net_generation(phases, expected) -> None:
payload = json.dumps(dict(
meter_source="PV", meter_w1=phases[0], meter_w2=phases[1],
meter_w3=phases[2], meter_kwh_imp=12.5))
run_lua(f'''
poll({json.dumps(payload)})
local pv = host._emitted.pv
assert(pv and #pv == 1, "expected one PV reading")
assert(pv[1].w == {expected}, "PV W = " .. tostring(pv[1].w))
assert(pv[1].w <= 0, "FTW rejects positive PV power")
assert(pv[1].generation_wh == 12500, "generation counter changed")
assert(host._metrics.pv_w_total.value == {expected}, "total metric differs")
assert(host._metrics.pv_l1_w.value == {-phases[0]}, "phase 1 changed")
assert(host._metrics.pv_l2_w.value == {-phases[1]}, "phase 2 changed")
assert(host._metrics.pv_l3_w.value == {-phases[2]}, "phase 3 changed")
''')


@pytest.mark.parametrize("phase", ("meter_w1", "meter_w2", "meter_w3"))
@pytest.mark.parametrize("bad_value", (None, "invalid", "1e400", "-1e400"))
def test_pixii_pv_does_not_turn_invalid_power_into_zero(phase, bad_value) -> None:
payload = dict(meter_source="PV", meter_w1=-1, meter_w2=-2, meter_w3=-3)
if bad_value is None:
del payload[phase]
else:
payload[phase] = bad_value
encoded = json.dumps(json.dumps(payload))
run_lua(f'''
poll({encoded})
assert(not host._emitted.pv, "invalid phase power emitted as PV")
assert(not host._metrics.pv_w_total, "invalid phase power emitted as total")
''')


def test_pixii_pv_sunset_idle_and_morning() -> None:
run_lua('''
poll('{"meter_source":"PV","meter_w1":100,"meter_w2":100,"meter_w3":100}')
for _ = 1, 5 do
poll('{"meter_source":"PV","meter_w1":-1,"meter_w2":-2,"meter_w3":-3}')
end
assert(#host._emitted.pv == 6, "night readings stopped")
for i = 2, 6 do
assert(host._emitted.pv[i].w == 0, "night reading must show no generation")
end
host._emitted = {}
driver_poll()
assert(not host._emitted.pv, "idle poll replayed old PV data")

host._mqtt_buffer = {{
topic = "pixii/status/test-pixii/meter",
payload = '{"meter_w":-750}',
}}
driver_poll()
assert(host._emitted.meter[1].w == -750, "grid export sign changed")
assert(not host._emitted.pv, "grid-only message refreshed PV")
poll('{"meter_source":"PV","meter_w1":"1000","meter_w2":"2000","meter_w3":"3000"}')
assert(host._emitted.pv[1].w == -6000, "morning production did not return")
''')
6 changes: 3 additions & 3 deletions index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -507,14 +507,14 @@ drivers:
size_bytes: 32519
sha256: "77e233e2d24e970ebff8eaacd67967f57b651677d25ce7b57a1f97c3eaeb71fe"
- name: "pixii_pv"
version: "0.3.2"
version: "0.3.3"
tier: core
protocol: mqtt
connectivity: local
ders: [pv, meter]
control: false
size_bytes: 6699
sha256: "7a27b937bdc845dff8d4032cb96ccca16242cc8378d1cbf5cfc8030660b871db"
size_bytes: 6980
sha256: "6b1a7c67e99a7b056d9a209800fc8ce3e63f47796846644eccaf8b31da3b988e"
- name: "remne_p1ib"
version: "0.1.0"
tier: community
Expand Down
6 changes: 3 additions & 3 deletions manifests/pixii_pv.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "pixii_pv"
version: "0.3.2"
version: "0.3.3"
tier: core
author: "Sourceful Labs AB"
protocol: mqtt
Expand All @@ -15,9 +15,9 @@ tested_devices:
notes: "Read-only Pixii telemetry: PV (external CTs) + grid meter (meter_w). Battery / SoC comes from the Modbus driver, not this one."
min_driver_version: "0.2.0"
min_host_version: "2.0.0"
size_bytes: 6699
size_bytes: 6980
dkb_id: "pixii_pv"
sha256: "7a27b937bdc845dff8d4032cb96ccca16242cc8378d1cbf5cfc8030660b871db"
sha256: "6b1a7c67e99a7b056d9a209800fc8ce3e63f47796846644eccaf8b31da3b988e"
signature: ""

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