From 358825a55edd37c4a8dce5f4cde48278c250379b Mon Sep 17 00:00:00 2001 From: Chris Muench Date: Wed, 6 Apr 2022 11:44:20 -0700 Subject: [PATCH 1/4] Blueprint for LG RESU Prime batteries --- .../lg_resu_prime_http/README.md | 11 + .../lg_resu_prime_http/firmware.lua | 245 ++++++++++++++++++ .../lg_resu_prime_http/manifest.yml | 107 ++++++++ 3 files changed, 363 insertions(+) create mode 100644 battery_management_systems/lg_resu_prime_http/README.md create mode 100644 battery_management_systems/lg_resu_prime_http/firmware.lua create mode 100644 battery_management_systems/lg_resu_prime_http/manifest.yml diff --git a/battery_management_systems/lg_resu_prime_http/README.md b/battery_management_systems/lg_resu_prime_http/README.md new file mode 100644 index 00000000..2de73bcb --- /dev/null +++ b/battery_management_systems/lg_resu_prime_http/README.md @@ -0,0 +1,11 @@ +# LG RESU (10|16)h Prime (http) + +This _Enapter Device Blueprint_ integrates **LG RESU 10h and 16h Prime** lithium battery control and monitoring via an undocumented http interface on the battery + +Use an Enapter Virtual UCM to load this blueprint and set the IP address of the LG Battery. + +Please ensure that your installer is connecting the LAN connection of the battery to your network. + +## References + +- [LG RESU Prime Battery product page](https://www.lgessbattery.com/us/home-battery/product-info.lg) diff --git a/battery_management_systems/lg_resu_prime_http/firmware.lua b/battery_management_systems/lg_resu_prime_http/firmware.lua new file mode 100644 index 00000000..3addc071 --- /dev/null +++ b/battery_management_systems/lg_resu_prime_http/firmware.lua @@ -0,0 +1,245 @@ +json = require 'json' + +-- Configuration variables must be also defined +-- in `write_configuration` command arguments in manifest.yml +IP_ADDRESS_CONFIG = 'ip_address' + +MYModel='' + +function main() + scheduler.add(30000, sendmyproperties) + scheduler.add(15000, sendmytelemetry) + + config.init({ + [IP_ADDRESS_CONFIG] = { type = 'string', required = true } + }) + +end + +-- To send data to Enapter Cloud use `enapter` variable as shown below. +function registration() +-- enapter.send_registration({ vendor = "C-Labs", model = "LG RESU PRIME" }) +end + +function sendmyproperties() + local properties = {} + local values, err = config.read_all() + if err then + enapter.log('cannot read config: '..tostring(err), 'error') + else + for name, val in pairs(values) do + properties[name] = val + end + end + if (MYModel~='') then + properties['model']=MYModel + end + enapter.send_properties(properties) +end + +function sendmytelemetry() + local json = require('json') + + 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..'/getbmsdata') + 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 tres=response.body + tres=string.gsub(tres, 'BMS DataItemValue', '{') + tres=string.gsub(tres, '', '", "') + tres=string.gsub(tres, '',' "') + tres=string.gsub(tres, '','": "') + tres=string.gsub(tres, '', '" }') + --enapter.log('Request succeeded: '..tres, 'info') + + local deco=json.decode(tres) + telemetry["battery_soc"] = tonumber(deco.SOC)/100 + telemetry["battery_power"]= tonumber(deco.Current) + telemetry["battery_temp"]= tonumber(deco.Temperature)/10 + telemetry["lastresponse"]='All Good' + local tcur=tonumber(deco.OperationModeStatus); + telemetry["battery_energy"]= tonumber(deco.SOH) + telemetry["battery_voltage"]= tonumber(deco.AvgCellVoltage)/100 + + if (MYModel=='') then + if (tonumber(deco.SOH)==10000) then + MYModel="LG RESU10 Prime" + else + MYModel="LG RESU16 Prime" + end + end + + if (tcur==0) then + telemetry["status"]='Off' + elseif (tcur==1) then + telemetry["status"]='Standby' + elseif (tcur==2) then + telemetry["status"]='Initializing' + elseif (tcur==3) then + telemetry["status"]='Charging' + elseif (tcur==4) then + telemetry["status"]='Discharging' + elseif (tcur==5) then + telemetry["status"]='Fault' + elseif (tcur==7) then + telemetry["status"]='Idle' + 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() diff --git a/battery_management_systems/lg_resu_prime_http/manifest.yml b/battery_management_systems/lg_resu_prime_http/manifest.yml new file mode 100644 index 00000000..5b0fc750 --- /dev/null +++ b/battery_management_systems/lg_resu_prime_http/manifest.yml @@ -0,0 +1,107 @@ +blueprint_spec: "device/1.0" +display_name: LG RESU Prime +icon: enapter-battery-storage + +communication_modules: + eth: + product: ENP-VIRTUAL + lua_file: firmware.lua + +properties: + model: + display_name: Device Model + type: string + enum: + - LG RESU10 Prime + - LG RESU16 Prime + ip_address: + display_name: IP Address + type: string + +telemetry: + battery_soc: + display_name: State of Charge + description: Current State of Charge + type: float + unit: precentage + battery_energy: + display_name: Battery Energy + description: Amount Energy of the Battery + type: float + unit: watts + battery_voltage: + display_name: Battery Voltage + type: float + unit: volt + battery_power: + display_name: Power Flow + description: Amount of power flowing in or out of the battery + type: float + unit: watts + battery_temp: + display_name: Battery Temperature + description: Current temperature of the battery + type: float + unit: celsius + status: + display_name: Battery status + type: string + enum: + - 'Off' + - Standby + - Initializing + - Charging + - Discharging + - Fault + - Idle + lastresponse: + display_name: Last Response + description: Last Response from battery + type: string + +alerts: + lowbattery: + severity: error + code: E001 + display_name: Low battery + description: Battery has less then 10% load + +command_groups: + connection: + display_name: Connection + +commands: + # Connection Config + write_configuration: + populate_values_command: read_configuration + display_name: Configure Connection + group: connection + ui: + icon: wrench-outline + arguments: + ip_address: + display_name: IP Address + description: IP address of the LG Prime Battery + type: string + required: true + read_configuration: + display_name: Read Connection Config + group: connection + ui: + icon: wrench-outline + +.cloud: + category: batteries + mobile_main_chart: battery_soc + mobile_telemetry: + - battery_soc + - battery_power + - battery_temp + - battery_energy + - battery_voltage + - operation_mode + mobile_charts: + - battery_soc + - battery_power + - battery_temp + - battery_energy From f1b1684d4ed52a87b90fadb70c8c37173ed48d36 Mon Sep 17 00:00:00 2001 From: Chris Muench Date: Wed, 6 Apr 2022 11:44:56 -0700 Subject: [PATCH 2/4] gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..cc6b14b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/.vs/marketplace.fork/v17/.suo From 3b4410dc992b954f7db5cfd80d98a9f7235c5ec8 Mon Sep 17 00:00:00 2001 From: Chris Muench Date: Wed, 6 Apr 2022 12:02:30 -0700 Subject: [PATCH 3/4] better status --- .../lg_resu_prime_http/firmware.lua | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/battery_management_systems/lg_resu_prime_http/firmware.lua b/battery_management_systems/lg_resu_prime_http/firmware.lua index 3addc071..645e4f24 100644 --- a/battery_management_systems/lg_resu_prime_http/firmware.lua +++ b/battery_management_systems/lg_resu_prime_http/firmware.lua @@ -16,9 +16,7 @@ function main() end --- To send data to Enapter Cloud use `enapter` variable as shown below. function registration() --- enapter.send_registration({ vendor = "C-Labs", model = "LG RESU PRIME" }) end function sendmyproperties() @@ -58,11 +56,8 @@ function sendmytelemetry() end local tres=response.body - tres=string.gsub(tres, 'BMS DataItemValue', '{') + -- http results in a html block that needs to be converted to json + tres=string.gsub(tres, 'BMS DataItemValue','{') tres=string.gsub(tres, '', '", "') tres=string.gsub(tres, '',' "') tres=string.gsub(tres, '','": "') @@ -72,8 +67,10 @@ function sendmytelemetry() local deco=json.decode(tres) telemetry["battery_soc"] = tonumber(deco.SOC)/100 telemetry["battery_power"]= tonumber(deco.Current) + local cur=tonumber(deco.Current) telemetry["battery_temp"]= tonumber(deco.Temperature)/10 telemetry["lastresponse"]='All Good' + -- this should have the correct battery status but shows only 0x0001 no matter of state local tcur=tonumber(deco.OperationModeStatus); telemetry["battery_energy"]= tonumber(deco.SOH) telemetry["battery_voltage"]= tonumber(deco.AvgCellVoltage)/100 @@ -101,6 +98,16 @@ function sendmytelemetry() elseif (tcur==7) then telemetry["status"]='Idle' end + +-- fallback since OperationModeStatus does not work correctly + if (cur==0) then + telemetry["status"]='Standby' + elseif (cur>0) then + telemetry["status"]='Charging' + else + telemetry["status"]='Discharging' + end + enapter.send_telemetry(telemetry) end end From 33333699c800ab3a5ce88e385a6fb0b673cbb03e Mon Sep 17 00:00:00 2001 From: Chris Muench Date: Thu, 12 May 2022 14:34:12 -0700 Subject: [PATCH 4/4] gitignore for vs files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cc6b14b8..50ed7e0d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ ################################################################################ /.vs/marketplace.fork/v17/.suo +/.vs