-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathfronius_smart_meter.lua
More file actions
181 lines (161 loc) · 6.03 KB
/
Copy pathfronius_smart_meter.lua
File metadata and controls
181 lines (161 loc) · 6.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
-- fronius_smart_meter.lua
-- Fronius Smart Meter (three-phase energy meter) driver
-- Emits: Meter (read-only)
-- Protocol: Modbus TCP (SunSpec), ALL HOLDING registers, F32 BE throughout
-- Ported from sourceful-hugin/device-support/drivers/lua/fronius_smart_meter.lua
-- Port notes (v2.1 API drift vs hugin):
-- host.log(msg) → host.log("info", msg)
-- host.decode_u32/i32 → _be variants
-- host.decode_f32 → inline IEEE-754 (two u16, big-endian words)
DRIVER = {
host_api_min = 1,
host_api_max = 1,
id = "fronius-smart-meter",
name = "Fronius Smart Meter",
manufacturer = "Fronius",
version = "1.0.0",
protocols = { "modbus" },
capabilities = { "meter" },
description = "Fronius Smart Meter three-phase energy meter via Modbus TCP (SunSpec).",
homepage = "https://www.fronius.com",
authors = { "FTW contributors" },
tested_models = { "Smart Meter 50kA-3", "Smart Meter 63A-3", "Smart Meter TS 65A-3" },
verification_status = "experimental",
verification_notes = "Ported from a reference implementation. Not yet verified against live hardware on a FTW site.",
connection_defaults = {
port = 502,
unit_id = 1,
},
}
--
-- Register map (all HOLDING, all SunSpec F32 BE pairs — hi word first):
-- 40074/40076/40078 — per-phase current (A)
-- 40082/40084/40086 — per-phase voltage (V)
-- 40096 — grid frequency (Hz)
-- 40098 — total AC power (W) positive = importing from grid
-- 40100/40102/40104 — per-phase power (W)
-- 40130 — total export energy (Wh) lifetime counter
-- 40138 — total import energy (Wh) lifetime counter
--
-- Sign convention (site/EMS):
-- meter.w : positive = importing from grid, negative = exporting
-- Fronius Smart Meter already reports import-positive, no flip needed.
--
-- This driver complements drivers/fronius.lua (inverter). Mark the meter
-- entry in config.yaml as `is_site_meter: true` when it's the household
-- grid-connection meter.
PROTOCOL = "modbus"
----------------------------------------------------------------------------
-- Local decoder (replaces host.decode_f32 from hugin v1.x)
----------------------------------------------------------------------------
-- Decode IEEE 754 single-precision float from two u16 registers,
-- big-endian word order: hi = first register, lo = second.
-- Returns 0 for NaN / ±Inf (SunSpec "not implemented" sentinel is 0x7FC00000).
local function decode_f32_be(hi, lo)
hi = hi % 0x10000
lo = lo % 0x10000
local bits = hi * 0x10000 + lo
local sign = 1
if bits >= 0x80000000 then
sign = -1
bits = bits - 0x80000000
end
local exp = math.floor(bits / 0x800000)
local frac = bits % 0x800000
if exp == 0xFF then
return 0 -- NaN or Inf → treat as not-present
end
local value
if exp == 0 then
-- Subnormal (or zero)
value = frac / 0x800000 * (2 ^ -126)
else
value = (1 + frac / 0x800000) * (2 ^ (exp - 127))
end
return sign * value
end
-- Helper: read a contiguous F32 BE pair at `addr` and return the decoded
-- float. On Modbus error, returns 0 so the poll cycle still emits a
-- well-shaped table.
local function read_f32(addr)
local ok, regs = pcall(host.modbus_read, addr, 2, "holding")
if ok and regs then
return decode_f32_be(regs[1], regs[2])
end
return 0
end
----------------------------------------------------------------------------
-- Initialization
----------------------------------------------------------------------------
function driver_init(config)
host.set_make("Fronius")
-- Smart Meter has no accessible serial block; set_sn is skipped.
-- device_id falls back to mac:<arp> or ep:<endpoint>.
end
----------------------------------------------------------------------------
-- Telemetry polling
----------------------------------------------------------------------------
function driver_poll()
-- Per-phase current (A)
local l1_a = read_f32(40074)
local l2_a = read_f32(40076)
local l3_a = read_f32(40078)
-- Per-phase voltage (V)
local l1_v = read_f32(40082)
local l2_v = read_f32(40084)
local l3_v = read_f32(40086)
-- Grid frequency (Hz)
local hz = read_f32(40096)
-- Total AC power (W) — Fronius: positive = import, matches site convention
local total_w = read_f32(40098)
-- Per-phase AC power (W)
local l1_w = read_f32(40100)
local l2_w = read_f32(40102)
local l3_w = read_f32(40104)
-- Lifetime energy counters (Wh)
local export_wh = read_f32(40130)
local import_wh = read_f32(40138)
host.emit("meter", {
w = total_w,
l1_w = l1_w,
l2_w = l2_w,
l3_w = l3_w,
l1_v = l1_v,
l2_v = l2_v,
l3_v = l3_v,
l1_a = l1_a,
l2_a = l2_a,
l3_a = l3_a,
hz = hz,
import_wh = import_wh,
export_wh = export_wh,
})
-- Diagnostics: long-format TS DB
host.emit_metric("meter_l1_w", l1_w)
host.emit_metric("meter_l2_w", l2_w)
host.emit_metric("meter_l3_w", l3_w)
host.emit_metric("meter_l1_v", l1_v)
host.emit_metric("meter_l2_v", l2_v)
host.emit_metric("meter_l3_v", l3_v)
host.emit_metric("meter_l1_a", l1_a)
host.emit_metric("meter_l2_a", l2_a)
host.emit_metric("meter_l3_a", l3_a)
host.emit_metric("grid_hz", hz)
return 5000
end
----------------------------------------------------------------------------
-- Control (READ-ONLY — meter exposes no writable registers)
----------------------------------------------------------------------------
function driver_command(action, power_w, cmd)
if action == "init" or action == "deinit" then
return true
end
host.log("warn", "Fronius Smart Meter: read-only driver, ignoring action=" .. tostring(action))
return false
end
function driver_default_mode()
-- Read-only: nothing to revert.
end
function driver_cleanup()
-- No cached state to clear.
end