Skip to content

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

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
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
17 changes: 17 additions & 0 deletions power_meters/span_io_panel/README.md
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
This [Enapter Device Blueprint](https://go.enapter.com/marketplace-readme) integrates **SPAN.IO Smart Panel** - breaker panel. The connection is set 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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Use the `Configure` command in the Enapter mobile or Web app to set up the LG RESU 10h/16h Prime:
- Use the `Configure` command in the Enapter mobile or Web app to set up the SPAN.IO Smart Panel:

- IP address (use either static IP or DHCP reservation);

## References

- [SPAN.IO](https://span.io)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- [SPAN.IO](https://span.io)
- [SPAN.IO Smart Panel product page](https://www.span.io/panel)

190 changes: 190 additions & 0 deletions power_meters/span_io_panel/firmware.lua
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"})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to manifest.yml ip_address also should be sent in properties

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()
Loading