From b63513673737b01ff0aad4fbe48c5d5c1d08c58a Mon Sep 17 00:00:00 2001 From: Tatyana Date: Fri, 30 Jun 2023 17:29:57 +0200 Subject: [PATCH] Add Smappee Infinity --- .marketplace/devices/devices.yml | 10 + .marketplace/vendors/icons/smappee.png | Bin 0 -> 5467 bytes .marketplace/vendors/vendors.yml | 5 + power_meters/smappee_infinity/README.md | 19 ++ power_meters/smappee_infinity/manifest.yml | 200 ++++++++++++++ .../smappee_lua/http_connection.lua | 248 ++++++++++++++++++ .../smappee_infinity/smappee_lua/main.lua | 107 ++++++++ 7 files changed, 589 insertions(+) create mode 100644 .marketplace/vendors/icons/smappee.png create mode 100644 power_meters/smappee_infinity/README.md create mode 100644 power_meters/smappee_infinity/manifest.yml create mode 100644 power_meters/smappee_infinity/smappee_lua/http_connection.lua create mode 100644 power_meters/smappee_infinity/smappee_lua/main.lua diff --git a/.marketplace/devices/devices.yml b/.marketplace/devices/devices.yml index a872ca54..d6d7057e 100644 --- a/.marketplace/devices/devices.yml +++ b/.marketplace/devices/devices.yml @@ -608,3 +608,13 @@ blueprint_options: - blueprint: gas_sensors/igd_toc_635 verification_level: verified + +- id: smappee_infinity + display_name: Smappee Infinity + description: All-in-one energy management system. + icon: enapter-home + vendor: igd + category: power_meters + blueprint_options: + - blueprint: power_meters/smappee_infinity + verification_level: verified diff --git a/.marketplace/vendors/icons/smappee.png b/.marketplace/vendors/icons/smappee.png new file mode 100644 index 0000000000000000000000000000000000000000..ab27ecf2863557946b37be4b65ee65dfec073e62 GIT binary patch literal 5467 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 Qjc|aanH}koiCfG+0M2l&!2kdN literal 0 HcmV?d00001 diff --git a/.marketplace/vendors/vendors.yml b/.marketplace/vendors/vendors.yml index e35024b4..fa57baad 100644 --- a/.marketplace/vendors/vendors.yml +++ b/.marketplace/vendors/vendors.yml @@ -197,3 +197,8 @@ display_name: International Gas Detectors Ltd icon_url: https://raw.githubusercontent.com/Enapter/marketplace/main/.marketplace/vendors/icons/igd.png website: https://www.internationalgasdetectors.com + +- id: smappee + display_name: Smappee + icon_url: https://raw.githubusercontent.com/Enapter/marketplace/main/.marketplace/vendors/icons/smappee.png + website: https://www.smappee.com/ diff --git a/power_meters/smappee_infinity/README.md b/power_meters/smappee_infinity/README.md new file mode 100644 index 00000000..829f1ae3 --- /dev/null +++ b/power_meters/smappee_infinity/README.md @@ -0,0 +1,19 @@ +# Smappee Infinity + +This [Enapter Device Blueprint](https://go.enapter.com/marketplace-readme) integrates **Smappee Infinity** - Smart energy management solution for any energy need - via [HTTP API](https://go.enapter.com/developers-enapter-http) implemented on [Enapter Virtual UCM](https://go.enapter.com/handbook-vucm). + +## Connect to Enapter + +- Sign up to Enapter Cloud using [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 [Enapter Gateway](https://go.enapter.com/handbook-gateway-setup) to run Virtual UCM. +- Create [Enapter Virtual UCM](https://go.enapter.com/handbook-vucm). +- [Upload](https://go.enapter.com/developers-upload-blueprint) this blueprint to Enapter Virtual UCM. +- Use the `Set Up Connection` command in the Enapter mobile or Web app to set up the Enel JuiceBox 32 communication parameters: + - [Your Client ID](https://go.enapter.com/smappee-devapi-docs); + - [Your Client secret](https://go.enapter.com/smappee-devapi-docs). + - [Your Smappee username](https://go.enapter.com/smappee-devapi-docs). + - [Your Smappee password](https://go.enapter.com/smappee-devapi-docs). + +## References + +- [Smappee Infinity product page](https://go.enapter.com/smappee-infinity-product) diff --git a/power_meters/smappee_infinity/manifest.yml b/power_meters/smappee_infinity/manifest.yml new file mode 100644 index 00000000..41f2ff02 --- /dev/null +++ b/power_meters/smappee_infinity/manifest.yml @@ -0,0 +1,200 @@ +blueprint_spec: device/1.0 +display_name: Smappee Infinity +description: Smart energy monitoring system +icon: enapter-home +vendor: smappee +license: MIT +author: enapter +contributors: + - anataty +support: + url: https://go.enapter.com/enapter-blueprint-support + email: support@enapter.com +verification_level: verified + +communication_module: + product: ENP-VIRTUAL + lua: + dir: smappee_lua + amalg_mode: nodebug + dependencies: + - enapter-ucm + +properties: + vendor: + type: string + display_name: Vendor + model: + type: string + display_name: Model + +telemetry: + status: + type: string + display_name: State + enum: + - ok + - warning + consumption_power: + type: float + unit: Wh + display_name: Consumption + voltage1: + type: float + unit: V + display_name: Voltage Line 1 + voltage2: + type: float + unit: V + display_name: Voltage Line 2 + voltage3: + type: float + unit: V + display_name: Voltage Line 3 + current1_PHASEA: + type: float + unit: A + display_name: Main Load Current phase A + current1_PHASEB: + type: float + unit: A + display_name: Main Load Current phase B + current1_PHASEC: + type: float + unit: A + display_name: Main Load Current phase C + active1_PHASEA: + type: float + unit: W + display_name: Main Load active power phase A + active1_PHASEB: + type: float + unit: W + display_name: Main Load active power phase B + active1_PHASEC: + type: float + unit: W + display_name: Main Load active power phase C + current2_PHASEA: + type: float + unit: A + display_name: Zone Carport/Cafe Current phase A + current2_PHASEB: + type: float + unit: A + display_name: Zone Carport/Cafe Current phase B + current2_PHASEC: + type: float + unit: A + display_name: Zone Carport/Cafe Current phase C + active2_PHASEA: + type: float + unit: W + display_name: Zone Carport/Cafe active power phase A + active2_PHASEB: + type: float + unit: W + display_name: Zone Carport/Cafe active power phase B + active2_PHASEC: + type: float + unit: W + display_name: Zone Carport/Cafe active power phase C + current3_PHASEA: + type: float + unit: A + display_name: Zone Hotel / Office Current phase A + current3_PHASEB: + type: float + unit: A + display_name: Zone Hotel / Office Current phase B + current3_PHASEC: + type: float + unit: A + display_name: Zone Hotel / Office Current phase C + active3_PHASEA: + type: float + unit: W + display_name: Zone Hotel / Office active power phase A + active3_PHASEB: + type: float + unit: W + display_name: Zone Hotel / Office active power phase B + active3_PHASEC: + type: float + unit: W + display_name: Zone Hotel / Office active power phase C + +alerts: + no_data: + severity: warning + display_name: No data from device + description: > + Can't read data from Smappee Infinity. + Please check connection parameters. + connection_error: + severity: warning + display_name: Connection Error + description: > + Please use "Set Up Connection" command to set up your + Smappee account configuration. + no_service_location_name: + severity: warning + display_name: No service location name + description: > + Please use "Set Up Connection" command to set up your + Smappee service location name. + invalid_service_location_id: + severity: warning + display_name: Incorrect service location name + description: > + Please double check Smappee service location name. + +command_groups: + connection: + display_name: Connection + +commands: + # TODO: mark commands containing secrets + write_configuration: + display_name: Set Up Connection + description: Set your Smappee account parameters + group: connection + populate_values_command: read_configuration + ui: + icon: file-document-edit-outline + arguments: + username: + display_name: Smappee username + type: string + required: true + password: + display_name: Smappee password + type: string + required: true + client_id: + display_name: Smappee Client ID + type: string + required: true + client_secret: + display_name: Smappee Client Secret + type: string + required: true + service_location_name: + display_name: Smappee Service Location name + type: string + required: false + read_configuration: + display_name: Read Connection Parameters + group: connection + ui: + icon: file-check-outline + +.cloud: + mobile_main_chart: consumption_power + mobile_charts: + - consumption_power + - active_power + - current + - voltage1 + - voltage2 + - voltage3 diff --git a/power_meters/smappee_infinity/smappee_lua/http_connection.lua b/power_meters/smappee_infinity/smappee_lua/http_connection.lua new file mode 100644 index 00000000..07ddea78 --- /dev/null +++ b/power_meters/smappee_infinity/smappee_lua/http_connection.lua @@ -0,0 +1,248 @@ +local SmappeeHTTP = {} + +local json = require('json') +local net_url = require('net.url') + +function SmappeeHTTP.new(username, password, client_secret, client_id, location_name) + assert( + type(username) == 'string', + 'username (arg #1) must be string, given: ' .. inspect(username) + ) + assert( + type(password) == 'string', + 'password (arg #2) must be string, given: ' .. inspect(password) + ) + assert( + type(client_secret) == 'string', + 'client_secret (arg #3) must be string, given: ' .. inspect(client_secret) + ) + assert( + type(client_id) == 'string', + 'client_id (arg #4) must be string, given: ' .. inspect(client_id) + ) + + local self = setmetatable({}, { __index = SmappeeHTTP }) + + self.username = username + self.password = password + self.client_secret = client_secret + self.client_id = client_id + self.location_name = location_name + + self.url = net_url.parse('https://app1pub.smappee.net/dev/v3') + self.client = http.client({ timeout = 5 }) + + return self +end + +function SmappeeHTTP:process_unauthorized(request_type, headers, url, body) + local request = http.request(request_type, url, body) + + if headers ~= nil then + for name, value in pairs(headers) do + request:set_header(name, value) + end + end + + local response, err = self.client:do_request(request) + + if err then + return nil, err + elseif response.code ~= 200 then + return nil, 'non-OK code: ' .. tostring(response.code) + else + return json.decode(response.body), nil + end +end + +function SmappeeHTTP:process_authorized(request_type, url, body) + local request = http.request(request_type, url, body) + + request:set_header('Authorization', 'Bearer ' .. self.access_token) + request:set_header('Content-Type', 'application/json') + + local response, err = self.client:do_request(request) + + if err then + return nil, err + elseif response.code ~= 200 then + return nil, 'non-OK code: ' .. tostring(response.code) + else + return json.decode(response.body), nil + end +end + +function SmappeeHTTP:set_token() + local body = { + grant_type = 'password', + client_id = self.client_id, + client_secret = self.client_secret, + username = self.username, + password = self.password, + } + + local headers = {} + headers['Content-Type'] = 'application/x-www-form-urlencoded' + + local response, err = self:process_unauthorized( + 'POST', headers, self.url .. 'oauth2/token', body + ) + if err then + return 'set_token failed: ' .. tostring(err) + end + + if response ~= nil then + self.access_token = response['access_token'] + self.new_token = response['refresh_token'] + if response['expires_in'] == nil then + return 'no_expire_time' + else + self.expires = response['expires_in'] + os.time() + end + else + return 'no_tokens_data' + end +end + +function SmappeeHTTP:refresh_token() + local body = { + grant_type = 'refresh_token', + client_id = self.client_id, + client_secret = self.client_secret, + refresh_token = self.new_token, + } + + local headers = {} + headers['Content-Type'] = 'application/x-www-form-urlencoded' + + local response, err = self:process_unauthorized( + 'POST', headers, self.url .. 'oauth2/token', body + ) + if err then + return 'resfresh_token failed: ' .. tostring(err) + end + + if response ~= nil then + self.access_token = response['access_token'] + self.new_token = response['refresh_token'] + if response['expires_in'] == nil then + return 'no_expire_time' + else + self.expires = response['expires_in'] + os.time() + end + else + return 'no_refresh_tokens_data' + end +end + +function SmappeeHTTP:set_service_locations() + local response, err = self:process_authorized('GET', self.url .. 'servicelocation') + if err then + return 'set_service_locations failed: ' .. tostring(err) + end + + if response ~= nil then + self.service_locations = response['serviceLocations'] + else + return 'no_data' + end +end + +function SmappeeHTTP:set_service_location_id() + local err = self:set_service_locations() + if err == nil then + if type(self.service_locations) == 'table' then + for _, location in ipairs(self.service_locations) do + for k, v in pairs(location) do + if k == 'name' and v == self.location_name then + self.service_location_id = location['serviceLocationId'] + end + end + end + if not self.service_location_id then + return 'no_service_location_id' + end + else + return 'service_locations_invalid_type' + end + else + return tostring(err) + end +end + +function SmappeeHTTP:set_metering_configuration() + local response, err = self:process_authorized( + 'GET', + self.url .. 'servicelocation/' .. self.service_location_id .. '/meteringconfiguration' + ) + if err then + return 'set_metering_configuration failed: ' .. tostring(err) + end + + if response ~= nil then + local inputs = {} + + for i, input in ipairs(response['measurements']) do + if not inputs[i] then + inputs[i] = {} + end + inputs[i]['name'] = input['name'] + for _, val in ipairs(input['channels']) do + if not inputs[i]['indexes'] then + inputs[i]['indexes'] = {} + end + inputs[i]['indexes'][val['phase']] = val['consumptionIndex'] + end + end + + self.inputs = inputs + else + return 'no_metering_data' + end +end + +function SmappeeHTTP:get_electricity_consumption(from, to) + local url = self.url / 'servicelocation' / self.service_location_id / 'consumption' + url:setQuery({ aggregation = 1, from = from, to = to }) + + local response, err = self:process_authorized('GET', url) + + if err then + return nil, 'get_electricity_consumption failed: ' .. tostring(err) + end + + local err = self:set_metering_configuration() + if err ~= nil then + return nil, err + end + + local telemetry = {} + if response ~= nil then + if response['serviceLocationId'] == self.service_location_id then + local data = response['consumptions'][1] + if data ~= nil then + telemetry['voltage1'] = data['lineVoltages'][1] + telemetry['voltage2'] = data['lineVoltages'][2] + telemetry['voltage3'] = data['lineVoltages'][3] + telemetry['consumption_power'] = data['consumption'] + + for i, input in ipairs(self.inputs) do + for phase, index in pairs(input['indexes']) do + telemetry['current' .. i .. '_' .. phase] = data['current'][index + 1] + telemetry['active' .. i .. '_' .. phase] = data['active'][index + 1] + end + end + telemetry['status'] = 'ok' + end + else + telemetry['status'] = 'warning' + telemetry['alerts'] = { 'invalid_service_location_id' } + end + end + telemetry['status'] = 'warning' + telemetry['alerts'] = { 'no_data' } + + return telemetry, nil +end + +return SmappeeHTTP diff --git a/power_meters/smappee_infinity/smappee_lua/main.lua b/power_meters/smappee_infinity/smappee_lua/main.lua new file mode 100644 index 00000000..22f98545 --- /dev/null +++ b/power_meters/smappee_infinity/smappee_lua/main.lua @@ -0,0 +1,107 @@ +local config = require('enapter.ucm.config') +local SmappeeHTTP = require('http_connection') + +-- Configuration variables must be also defined +-- in `write_configuration` command arguments in manifest.yml +USERNAME = 'username' +PASSWORD = 'password' +CLIENT_SECRET = 'client_secret' +CLIENT_ID = 'client_id' +SERVICE_LOCATION_NAME = 'service_location_name' + +-- holds global Smappee connection +local smappee + +-- Initiate device firmware. Called at the end of the file. +function main() + scheduler.add(30000, send_properties) + scheduler.add(2000, send_telemetry) + + config.init({ + [USERNAME] = { type = 'string', required = true }, + [PASSWORD] = { type = 'string', required = true }, + [CLIENT_SECRET] = { type = 'string', required = true }, + [CLIENT_ID] = { type = 'string', required = true }, + [SERVICE_LOCATION_NAME] = { type = 'string', required = true }, + }, + { + after_write = function(args) + if args.service_location_name == nil then + return "service_location_name is required" + else + smappee:refresh_token() + smappee:set_service_location_id() + end + end + }) +end + +function send_properties() + enapter.send_properties({ + vendor = 'Smappee', + model = 'Infinity', + }) +end + +function send_telemetry() + local smappee, err = connect_smappee() + if err then + enapter.log("Can't connect to Smappee: " .. err) + enapter.send_telemetry({ + status = 'warning', + alerts = { 'connection_error' }, + }) + return + else + local location_name = config.read(SERVICE_LOCATION_NAME) + if tostring(location_name) ~= '' then + smappee:set_service_location_id(location_name, smappee:get_service_locations()) + + local telemetry, err = smappee:get_electricity_consumption((os.time() - 600) * 1000, os.time() * 1000) + if err == nil then + enapter.send_telemetry(telemetry) + else + enapter.log(err, 'error', true) + enapter.send_telemetry({ status = 'warning', alerts = 'no_data' }) + end + end + end +end + +function connect_smappee() + if smappee and smappee.expires ~= nil then + if smappee.expires <= os.time() then + local err = smappee:refresh_token() + if err ~= nil then + return nil, err + end + end + + return smappee, nil + end + + local values, err = config.read_all() + if err then + enapter.log('cannot read config: ' .. tostring(err), 'error') + return nil, 'cannot_read_config' + else + local client_secret, client_id = values[CLIENT_SECRET], values[CLIENT_ID] + local username, password = values[USERNAME], values[PASSWORD] + local location_name = values[SERVICE_LOCATION_NAME] + + if not client_secret or not client_id or not username or not password or not location_name then + return nil, 'not_configured' + else + smappee = SmappeeHTTP.new(username, password, client_secret, client_id) + + local err = smappee:set_token() + if err == nil then + return smappee, nil + else + return nil, err + end + end + end +end + +main()