Skip to content

Commit

Permalink
Add Deye Inverter (Solarman) (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
anataty authored Jul 21, 2023
1 parent 7a5269f commit 45ebae5
Show file tree
Hide file tree
Showing 8 changed files with 776 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .marketplace/devices/devices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,13 @@
blueprint_options:
- blueprint: gas_sensors/igd_toc_635
verification_level: verified

- id: deye_inverter-sun-10k-sg04lp3-eu
display_name: Deye Inverter SUN-10k-SG04LP3-EU
description: Three-phase hybrid inverter.
icon: enapter-inverter-solar
vendor: deye
category: solar_inverters
blueprint_options:
- blueprint: solar_inverters/deye_sun-10k-sg04lp3-eu_solarman
verification_level: verified
Binary file added .marketplace/vendors/icons/deye.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .marketplace/vendors/icons/solarman.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions .marketplace/vendors/vendors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: deye
display_name: Deye
icon_url: https://raw.githubusercontent.com/Enapter/marketplace/main/.marketplace/vendors/icons/deye.png
website: https://www.deyeinverter.com/
20 changes: 20 additions & 0 deletions solar_inverters/deye_sun-10k-sg04lp3-eu_solarman/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Deye SUN-10k-SG04LP3-EU (HTTP)

This [Enapter Device Blueprint](https://go.enapter.com/marketplace-readme) integrates **Deye Inverter SUN-10k-SG04LP3-EU** - three-phase hybrid inverter connected to Solarman system - 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 following communication parameters:
- Your Solarman account App ID;
- Your Solarman account App Secret;
- Your Solarman account username;
- Your Solarman account password;
- Inverter serial number;

## References

- [Deye Inverter product page](https://www.deyeinverter.com/product/hybrid-inverter-1/sun5-6-8-10-12ksg04lp3.html)
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
local SolarmanHTTP = {}

local json = require('json')
local net_url = require('net.url')
local sha256 = require('hashings.sha256')

function SolarmanHTTP.new(app_id, app_secret, email, password, device_sn, org_name, optional)
assert(type(app_id) == 'string', 'app_id (arg #1) must be string, given: ' .. inspect(app_id))
assert(
type(app_secret) == 'string',
'app_secret (arg #2) must be string, given: ' .. inspect(app_secret)
)
assert(type(email) == 'string', 'email (arg #3) must be string, given: ' .. inspect(email))
assert(
type(password) == 'string',
'password (arg #4) must be string, given: ' .. inspect(password)
)
assert(
type(device_sn) == 'string',
'device_sn (arg #5) must be string, given: ' .. inspect(device_sn)
)
assert(
type(org_name) == 'string',
'org_name (arg #6) must be string, given: ' .. inspect(org_name)
)

local self = setmetatable({}, { __index = SolarmanHTTP })

if email ~= nil then
self.email = email
elseif optional.username ~= nil then
self.username = optional.username
elseif optional.mobile ~= nil then
if optional.coutry_code ~= nil then
self.country_code = optional.coutry_code
else
return 'country code must be provided along with mobile number'
end
else
return nil, 'one of: email, username or mobile with country code must be provided'
end

self.password = password
self.app_secret = app_secret
self.app_id = app_id
self.device_sn = device_sn
self.org_name = org_name

self.url = 'https://globalapi.solarmanpv.com/'
self.client = http.client({ timeout = 5 })

return self
end

function SolarmanHTTP: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 SolarmanHTTP: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 SolarmanHTTP:set_token()
local body = {}

if self.email ~= nil then
body.email = self.email
elseif self.username ~= nil then
body.username = self.username
elseif self.mobile ~= nil then
if self.country_code ~= nil then
body.mobile = self.mobile
body.countryCode = self.country_code
else
return 'country code must be provided along with mobile number'
end
else
return 'one of: email, username or mobile with country code must be provided'
end

body.appSecret = self.app_secret
body.orgId = self.org_id
body.password = string.lower(sha256:new(self.password):hexdigest())

local url = net_url.parse(self.url) / 'account' / 'v1.0' / 'token'
url:setQuery({ appId = self.app_id })

local headers = {}
headers['Content-Type'] = 'application/json'

local response, err = self:process_unauthorized('POST', headers, tostring(url), json.encode(body))
if err then
return 'set_token failed: ' .. tostring(err)
end

if response ~= nil then
if response['success'] == false then
return response['msg']
end
if self.access_token ~= response['access_token'] and self.access_token ~= nil then
enapter.log('Bussiness access tokens are obtained', 'info')
else
enapter.log('Tokens are obtained', 'info')
end
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 SolarmanHTTP:bussiness_relation()
local url = net_url.parse(self.url) / 'account' / 'v1.0' / 'info'

local body = ''

local response, err = self:process_authorized('POST', tostring(url), body)

if err then
return nil, 'bussiness_relation failed: ' .. tostring(err)
end

if response ~= nil then
if response['success'] then
if response['orgInfoList'] ~= nil then
for _, org in pairs(response['orgInfoList']) do
if org['companyName'] == self.org_name then
self.org_id = org['companyId']
break
end
end
else
return 'empty orgInfoList'
end
else
return response['msg']
end
else
return 'no response'
end
end

function SolarmanHTTP:get_realtime_data()
local url = net_url.parse(self.url) / 'device' / 'v1.0' / 'currentData'

local body = json.encode({
deviceSn = self.device_sn,
})

local response, err = self:process_authorized('POST', tostring(url), body)

if err then
return nil, 'get_realtime_data failed: ' .. tostring(err)
end

local function map_by_key(t)
local tt = {}
for _, el in pairs(t) do
if tonumber(el.value) then
tt[el.key] = tonumber(el.value)
else
tt[el.key] = el.value
end
end
return tt
end

local telemetry = {}
if response ~= nil then
if response['success'] then
if response['dataList'] ~= nil then
local metrics = map_by_key(response['dataList'])
telemetry['DV1'] = metrics['DV1']
telemetry['DC1'] = metrics['DC1']
telemetry['DP1'] = metrics['DP1']
telemetry['DV2'] = metrics['DV2']
telemetry['DC2'] = metrics['DC2']
telemetry['DP2'] = metrics['DP2']
telemetry['S_P_T'] = metrics['S_P_T']
telemetry['G_V_L1'] = metrics['G_V_L1']
telemetry['G_C_L1'] = metrics['G_C_L1']
telemetry['G_P_L1'] = metrics['G_P_L1']
telemetry['G_V_L2'] = metrics['G_V_L2']
telemetry['G_C_L2'] = metrics['G_C_L2']
telemetry['G_P_L2'] = metrics['G_P_L2']
telemetry['G_V_L3'] = metrics['G_V_L3']
telemetry['G_C_L3'] = metrics['G_C_L3']
telemetry['G_P_L3'] = metrics['G_P_L3']
telemetry['PG_F1'] = metrics['PG_F1']
telemetry['PG_Pt1'] = metrics['PG_Pt1']
telemetry['CT1_P_E'] = metrics['CT1_P_E']
telemetry['CT2_P_E'] = metrics['CT2_P_E']
telemetry['CT3_P_E'] = metrics['CT3_P_E']
telemetry['CT_T_E'] = metrics['CT_T_E']
telemetry['L_F'] = metrics['L_F']
telemetry['LPP_A'] = metrics['LPP_A']
telemetry['LPP_B'] = metrics['LPP_B']
telemetry['LPP_C'] = metrics['LPP_C']
telemetry['LPP_C'] = metrics['LPP_C']
telemetry['E_Puse_t1'] = metrics['E_Puse_t1']
telemetry['B_V1'] = metrics['B_V1']
telemetry['B_C1'] = metrics['B_C1']
telemetry['B_P1'] = metrics['B_P1']
telemetry['B_left_cap1'] = metrics['B_left_cap1']
telemetry['ST_PG1'] = metrics['ST_PG1']
telemetry['B_ST1'] = metrics['B_ST1']
telemetry['Etdy_use1'] = metrics['Etdy_use1']
telemetry['Etdy_dcg1'] = metrics['Etdy_dcg1']
telemetry['Etdy_ge1'] = metrics['Etdy_ge1']
telemetry['GRID_RELAY_ST1'] = metrics['GRID_RELAY_ST1']
telemetry['status'] = 'ok'
telemetry['alerts'] = {}
else
telemetry['status'] = 'warning'
telemetry['alerts'] = { 'no_data' }
end
else
telemetry['status'] = 'warning'
telemetry['alerts'] = { 'invalid_request' }
return telemetry, response['msg']
end
else
telemetry['status'] = 'warning'
telemetry['alerts'] = { 'no_response' }
end

return telemetry, nil
end

return SolarmanHTTP
Loading

0 comments on commit 45ebae5

Please sign in to comment.