-
Notifications
You must be signed in to change notification settings - Fork 13
Initial Span Smart Breaker Panel support as Power Meter #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
358825a
f1b1684
3b4410d
69e1696
3333369
8fc82e2
e6ee27c
e1adbac
114080a
76e79c9
33b3cfd
78d7e9b
b81f0c1
b514bad
244facb
b8358e2
69b1f36
93a1da1
38a9b38
c7d9eb2
17ed005
a775fda
7fb3f63
c6f34ae
4974a12
8acd279
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,17 @@ | ||||||
# SPAN.io Smart Panel | ||||||
|
||||||
This [Enapter Device Blueprint](https://go.enapter.com/marketplace-readme) integrates **SPAN.IO Smart Panels** - breaker panel. The connection is via standard http REST calls. | ||||||
|
||||||
## Connect to Enapter | ||||||
|
||||||
- Sign up to the Enapter Cloud using the [Web](https://cloud.enapter.com/) or mobile app ([iOS](https://apps.apple.com/app/id1388329910), [Android](https://play.google.com/store/apps/details?id=com.enapter&hl=en)). | ||||||
- Use the [Enapter Gateway](https://handbook.enapter.com/software/gateway/2.0.0/setup/) to run the Virtual UCM. | ||||||
- Create the [Enapter Virtual UCM](https://handbook.enapter.com/software/software.html#%F0%9F%92%8E-virtual-ucm). | ||||||
- [Upload](https://developers.enapter.com/docs/tutorial/uploading-blueprint/) this blueprint to ENP-VIRTUAL. | ||||||
- Please ensure that your installer is connecting the LAN connection of the battery to your network. | ||||||
- Use the `Configure` command in the Enapter mobile or Web app to set up the LG RESU 10h/16h Prime: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
- IP address (use either static IP or DHCP reservation); | ||||||
|
||||||
## References | ||||||
|
||||||
- [SPAN.IO](https://span.io) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
json = require 'json' | ||
|
||
-- Configuration variables must be also defined | ||
-- in `write_configuration` command arguments in manifest.yml | ||
IP_ADDRESS_CONFIG = 'ip_address' | ||
|
||
function main() | ||
scheduler.add(30000, send_properties) | ||
scheduler.add(15000, sendmytelemetry) | ||
|
||
config.init({ | ||
[IP_ADDRESS_CONFIG] = { type = 'string', required = true } | ||
}) | ||
end | ||
|
||
function send_properties() | ||
enapter.send_properties({vendor = "SPAN.IO", model = "Smart Breaker Panel"}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to manifest.yml |
||
end | ||
|
||
function sendmytelemetry() | ||
local values, err = config.read_all() | ||
if err then | ||
enapter.log('cannot read config: '..tostring(err), 'error') | ||
return nil, 'cannot_read_config' | ||
else | ||
local ip_address = values[IP_ADDRESS_CONFIG] | ||
local telemetry = {} | ||
|
||
local response, err = http.get('http://'..ip_address..'/api/v1/circuits') | ||
if err then | ||
enapter.log('Cannot do request: '..err, 'error') | ||
return | ||
elseif response.code ~= 200 then | ||
enapter.log('Request returned non-OK code: '..response.code, 'error') | ||
return | ||
end | ||
|
||
local deco=json.decode(response.body) | ||
local cnt=0 | ||
for key, circuit in pairs(deco.circuits) do | ||
cnt = cnt + 1 | ||
telemetry['breaker_'..cnt] = tonumber(circuit.instantPowerW) | ||
telemetry['breaker_'..cnt..'_status'] = circuit.relayState | ||
if cnt < 3 then | ||
telemetry['breaker_'..cnt..'_name'] = circuit.name | ||
telemetry['breaker_'..cnt..'_id'] = key | ||
end | ||
end | ||
enapter.send_telemetry(telemetry) | ||
end | ||
end | ||
|
||
--------------------------------- | ||
-- Stored Configuration API | ||
--------------------------------- | ||
|
||
config = {} | ||
|
||
-- Initializes config options. Registers required UCM commands. | ||
-- @param options: key-value pairs with option name and option params | ||
-- @example | ||
-- config.init({ | ||
-- address = { type = 'string', required = true }, | ||
-- unit_id = { type = 'number', default = 1 }, | ||
-- reconnect = { type = 'boolean', required = true } | ||
-- }) | ||
function config.init(options) | ||
assert(next(options) ~= nil, 'at least one config option should be provided') | ||
assert(not config.initialized, 'config can be initialized only once') | ||
for name, params in pairs(options) do | ||
local type_ok = params.type == 'string' or params.type == 'number' or params.type == 'boolean' | ||
assert(type_ok, 'type of `'..name..'` option should be either string or number or boolean') | ||
end | ||
|
||
enapter.register_command_handler('write_configuration', config.build_write_configuration_command(options)) | ||
enapter.register_command_handler('read_configuration', config.build_read_configuration_command(options)) | ||
|
||
config.options = options | ||
config.initialized = true | ||
end | ||
|
||
-- Reads all initialized config options | ||
-- @return table: key-value pairs | ||
-- @return nil|error | ||
function config.read_all() | ||
local result = {} | ||
|
||
for name, _ in pairs(config.options) do | ||
local value, err = config.read(name) | ||
if err then | ||
return nil, 'cannot read `'..name..'`: '..err | ||
else | ||
result[name] = value | ||
end | ||
end | ||
|
||
return result, nil | ||
end | ||
|
||
-- @param name string: option name to read | ||
-- @return string | ||
-- @return nil|error | ||
function config.read(name) | ||
local params = config.options[name] | ||
assert(params, 'undeclared config option: `'..name..'`, declare with config.init') | ||
|
||
local ok, value, ret = pcall(function() | ||
return storage.read(name) | ||
end) | ||
|
||
if not ok then | ||
return nil, 'error reading from storage: '..tostring(value) | ||
elseif ret and ret ~= 0 then | ||
return nil, 'error reading from storage: '..storage.err_to_str(ret) | ||
elseif value then | ||
return config.deserialize(name, value), nil | ||
else | ||
return params.default, nil | ||
end | ||
end | ||
|
||
-- @param name string: option name to write | ||
-- @param val string: value to write | ||
-- @return nil|error | ||
function config.write(name, val) | ||
local ok, ret = pcall(function() | ||
return storage.write(name, config.serialize(name, val)) | ||
end) | ||
|
||
if not ok then | ||
return 'error writing to storage: '..tostring(ret) | ||
elseif ret and ret ~= 0 then | ||
return 'error writing to storage: '..storage.err_to_str(ret) | ||
end | ||
end | ||
|
||
-- Serializes value into string for storage | ||
function config.serialize(_, value) | ||
if value then | ||
return tostring(value) | ||
else | ||
return nil | ||
end | ||
end | ||
|
||
-- Deserializes value from stored string | ||
function config.deserialize(name, value) | ||
local params = config.options[name] | ||
assert(params, 'undeclared config option: `'..name..'`, declare with config.init') | ||
|
||
if params.type == 'number' then | ||
return tonumber(value) | ||
elseif params.type == 'string' then | ||
return value | ||
elseif params.type == 'boolean' then | ||
if value == 'true' then | ||
return true | ||
elseif value == 'false' then | ||
return false | ||
else | ||
return nil | ||
end | ||
end | ||
end | ||
|
||
function config.build_write_configuration_command(options) | ||
return function(ctx, args) | ||
for name, params in pairs(options) do | ||
if params.required then | ||
assert(args[name], '`'..name..'` argument required') | ||
end | ||
|
||
local err = config.write(name, args[name]) | ||
if err then ctx.error('cannot write `'..name..'`: '..err) end | ||
end | ||
end | ||
end | ||
|
||
function config.build_read_configuration_command(_config_options) | ||
return function(ctx) | ||
local result, err = config.read_all() | ||
if err then | ||
ctx.error(err) | ||
else | ||
return result | ||
end | ||
end | ||
end | ||
|
||
main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.