Charge Amps - can this be done in Homey?

Will soon after a couple of years return to using Homey Pro as the controller of my Smart Home, just waiting for the delivery of the new HP 2023.

Been using Fibaro HC3 for several years and did (with the help of some friends) develop a QuickApp for the HC3 to control a ChargeAmps car charger. Have now started to look at how to develop Apps for the Homey but not sure if what was doable on the Fibaro HC3 actually are doable on the Homey when it comes to communicating with other vendors API.

The control part of the Charge Amps are straight forward and rather “simple”. The tricky part is to setup the actual connection and then maintain it open, or at least that was the tricky part in the Fibaro HC3 implementation.

Short description to establish a connection with ChargeAmps:

Login with UserId+Password+APIkey

This will give you a Token that is valid for 120minutes, this Token will then be used in all commands that you send to the Charge Amps API

To be able to continue to communicate you need to within the 120minutes do a refresh of the Token.

In Fibaro the code looked like this:
– Login to the ChargeAmps API.

function QuickApp:CAlogin(email,pwd,APIkey)
         self:apiCallCA{ignoreTokenCheck=true, method='POST',
                   path='/v4/auth/login',
                   headers={["apiKey"] = APIkey, ["Content-Type"] = "application/json", ["accept"] = "*/*"},
                   data={email=email,password=pwd},
        response=function(data)
            tokenCA = data.token
            refreshTokenCA = data.refreshToken
            setInterval(function() self:renewToken() end,1000*7080)
            self:GetOwned()
        end,
        error=function(msg) self:error("Failed login",msg)
        end}
end

Charge Amps API Call function

function QuickApp:apiCallCA(args)
    if (not args.ignoreTokenCheck) and tokenCA==nil then error("Not logged in") end
    local method = args.method
    local path = args.path
    local data = args.data
    local response = args.response
    local err = args.error
    local headers = {
                   ["accept"] = "application/json", 
                   ["Authorization"] = type(tokenCA)=='string' and ("Bearer " ..tokenCA) or nil
                   }
    for n,v in pairs(args.headers or {}) do headers[n]=v end
    http = net.HTTPClient()
    http:request("https://eapi.charge.space/api"..path, 
        {
        options = {
                 method = method or "GET",
                 headers = headers,
                 data = data and json.encode(data) or nil
                 },
        success = function(resp)
            local data = json.decode(resp.data)
            if response then response(data) end
        end,
        error = function(msg) 
            self:error(path,msg) 
            if err then err(msg) end
        end})
end

Renew the Charge Amps token (done every 118min (token expires after 120min))

function QuickApp:renewToken()
    if not refreshTokenCA then error("Not logged in") end
    self:debug("ChargeAmps: Doing the RENEW token function")
    self:apiCallCA{method="POST",
                   path="/v4/auth/refreshtoken",
                   headers={["Content-Type"] = "application/json", ["Authorization"] = "Bearer " ..tokenCA, ["Content-Type"] = "application/json"},
                   data={token=tokenCA,refreshToken=refreshTokenCA},
    response=function(data)
        tokenCA = data.token
        refreshTokenCA = data.refreshToken
    end,
    error=function(msg) 
        self:error("Failed renewing token",msg) 
    end}
end

So the question I have is if the smart people in here think it is doable to do this in the Homey and if anyone have had any experience from the same type of communication setup?

The ideal way would be to create an app https://apps.developer.homey.app/

Yes, that was the plan…

The question I have is if it is possible to do this in an App in Homey (Establish API connection, get token, refresh the Token every 118min, etc. etc.)

And if someone hade developed any similar App and was willing to share the code to shorten my learning curve :slight_smile:

Most of my apps do this in one way or another and your welcome to look at them AdyRock (AdyRock) / Repositories · GitHub

3 Likes

@Adrian_Rockall you truly have a vast number of interesting Apps…

I guess building an Homey App for the Charge Amps car charger will need to be based on an OAuth2 connection to ChargeAmps External REST API.

Guess the best start is to try to create a very simple App that just establishes the connection and perhaps retrieves a status if the Charger is on/off?

But even this “simple” task looks really hard for someone that has zero experience of writing Node.js code. Have tried to walk through the code of some of your Apps, but struggle to figure out how to start. Doing this in LUA code in the Fibaro HC3 was a much simpler task and all connectivity and total control of the charger was done in around just 200 lines of code.

-- -- -- -- -- -- -- -- DEFINITION OF VARIABLES -- -- -- -- -- -- -- --
local tokenCA,refreshTokenCA,idCA,fwCA,hwCA,dimmerCA,downLightCA
local CurrentCA 
local function debug(...) if DEBUG then quickApp:debug(string.format(...)) end end

btnMapOutlet = {
   lbl_OutletON = {args={a=2, b=1}, fun=changeOutlet},
   lbl_OutletSCHEMA = {args={a=2, b=2}, fun=changeOutlet},
   lbl_OutletOFF = {args={a=2, b=0}, fun=changeOutlet}}
btnMapLight = {
   lbl_LightOn  = {args={a=true}, fun=Light}, 
   lbl_LightOFF = {args={a=false}, fun=Light}}
btnMapLED = {
   lbl_LEDoff  = {args={a="Off"}, fun=ChangeLED}, 
   lbl_LEDlow = {args={a="Low"}, fun=ChangLED},
   lbl_LEDmedium  = {args={a="Medium"}, fun=ChangeLED}, 
   lbl_LEDhigh = {args={a="High"}, fun=ChangeLED}}

-- -- -- -- -- -- -- CODE FOR DEVICE CONTROL -- -- -- -- -- -- -- -- --
- - Charge Amps API Call function
function QuickApp:apiCallCA(args)
    if (not args.ignoreTokenCheck) and tokenCA==nil then error("Not logged in") end
    local method = args.method
    local path = args.path
    local data = args.data
    local response = args.response
    local err = args.error
    local headers = {
                   ["accept"] = "application/json", 
                   ["Authorization"] = type(tokenCA)=='string' and ("Bearer " ..tokenCA) or nil
                   }
    for n,v in pairs(args.headers or {}) do headers[n]=v end
    http = net.HTTPClient()
    --self:debug("API Calling: ",path)
    http:request("https://eapi.charge.space/api"..path, 
        {
        options = {
                 method = method or "GET",
                 headers = headers,
                 data = data and json.encode(data) or nil
                 },
        success = function(resp)
            --self:debug("Status:",path,resp.status)
            local data = json.decode(resp.data)
            if response then response(data) end
        end,
        error = function(msg) 
            self:error(path,msg) 
            if err then err(msg) end
        end})
end

-- Renew the Charge Amps token (done every 118min (token expires after 120min))
function QuickApp:renewToken()
    if not refreshTokenCA then error("Not logged in") end
    self:debug("ChargeAmps: Doing the RENEW token function")
    self:apiCallCA{method="POST",
                   path="/v4/auth/refreshtoken",
                   headers={["Content-Type"] = "application/json", ["Authorization"] = "Bearer " ..tokenCA, ["Content-Type"] = "application/json"},
                   data={token=tokenCA,refreshToken=refreshTokenCA},
    response=function(data)
        tokenCA = data.token
        refreshTokenCA = data.refreshToken
    end,
    error=function(msg) 
        self:error("Failed renewing token",msg) 
    end}
end

-- Login to the ChargeAmps API.
function QuickApp:CAlogin(email,pwd,APIkey)
    self:apiCallCA{ignoreTokenCheck=true, method='POST',
                   path='/v4/auth/login',
                   headers={["apiKey"] = APIkey, ["Content-Type"] = "application/json", ["accept"] = "*/*"},
                   data={email=email,password=pwd},
        response=function(data)
            tokenCA = data.token
            refreshTokenCA = data.refreshToken
            setInterval(function() self:renewToken() end,1000*7080)
            self:GetOwned()
        end,
        error=function(msg) self:error("Failed login",msg)
        end}
end

-- Turn ON the Charger
function QuickApp:ChargerON()
    self:apiCallCA{method='PUT',
                   path='/v4/chargepoints/'..idCA..'/connectors/1/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA,["Content-Type"] = "application/json"},
                   data = {chargePointId=idCA, connectorId=1, maxCurrent=CurrentCA, rfidLock=false ,mode=1, cableLock=false},
    response=function(data)
        self:updateView("lbl_Charger", "text", "<span class=\"section-title\"><b><font color=#000000>CHARGER Status:<b><font color=#6aa84f> On </font></b>")
    end}
end

-- Turn ON schemas for Charger
function QuickApp:ChargerSCHEMA()
    self:apiCallCA{method='PUT',
                   path='/v4/chargepoints/'..idCA..'/connectors/1/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA,["Content-Type"] = "application/json"},
                   data = {chargePointId=idCA, connectorId=1, maxCurrent=CurrentCA, rfidLock=false ,mode=2, cableLock=false},
    response=function(data)
    self:updateView("lbl_Charger", "text", "<span class=\"section-title\"><b><font color=#000000>CHARGER Status:<b><font color=#5794f2> Schema </font></b>")
    end}
end

-- Turn OFF the charger
function QuickApp:ChargerOFF()
    self:apiCallCA{method='PUT',
                   path='/v4/chargepoints/'..idCA..'/connectors/1/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA,["Content-Type"] = "application/json"},
                   data = {chargePointId=idCA, connectorId=1, maxCurrent=CurrentCA, rfidLock=false ,mode=0, cableLock=false},
    response=function(data)
    self:updateView("lbl_Charger", "text", "<span class=\"section-title\"><b><font color=#000000>CHARGER Status:<b><font color=#f44336> Off</font></b>")
    end}
end

-- Change the 'Outlet' based on which button is pressed (Variables defined above)
function QuickApp:changeOutlet(ev)
    local args = btnMapOutlet[ev.elementName].args 
    QA:apiCallCA{method='PUT',
                   path='/v4/chargepoints/'..idCA..'/connectors/'..args.a..'/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA,["Content-Type"] = "application/json"},
                   data = {chargePointId=idCA, connectorId=2, maxCurrent=CurrentCA, rfidLock=false ,mode=args.b, cableLock=false},
    response=function(data)
     self:GetChargerStatus()
    end}
end

-- Change 'Down Light' based on button (Variables defined above)
function QuickApp:Light(ev)
    local args = btnMapLight[ev.elementName].args
    self:apiCallCA{method='PUT',
                   path='/v4/chargepoints/'..idCA..'/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA,["Content-Type"] = "application/json"},
                   data = {id=idCA,dimmer=dimmerCA,downLight=args.a,maxCurrent=null},
    response=function(data)
        self:GetInfo()
    end}
end

-- Change 'LED RingLight' based on which button is pressed. (Variables defined above)
function QuickApp:ChangeLED(ev)
    local args = btnMapLED[ev.elementName].args
    self:apiCallCA{method='PUT',
                   path='/v4/chargepoints/'..idCA..'/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA,["Content-Type"] = "application/json"},
                   data = {id=idCA,dimmer=args.a,downLight=downLightCA,maxCurrent=null},
    response=function(data)
        self:GetInfo()
    end}
end

-- Get Charge Amps ID
function QuickApp:GetOwned()
    self:apiCallCA{method='GET',
                   path='/v4/chargepoints/owned',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA,["Content-Type"] = "application/json"},
    response=function(data)
            idCA = data[1].id
            fwCA = data[1].firmwareVersion
            hwCA = data[1].hardwareVersion
            self:updateView("lbl_ChargePointID", "text", "<span class=\"section-title\"><b><font color=#000000> ChargePoint ID: <font color=#5794f2>"..idCA.." </font></b>")
            self:updateView("lbl_VersionsCA", "text", " Firmware version: "..fwCA.."   -   Hardware version: "..hwCA.." ")
            self:debug("Got ChargePoint ID: ", idCA)
            self:GetInfo()
    end}
end

-- Get Charge Amps Information
function QuickApp:GetInfo()
    self:apiCallCA{method='GET',
                   path='/v4/chargepoints/'..idCA..'/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA},
    response=function(data)
            dimmerCA = data.dimmer
            downLightCA = data.downLight
            if downLightCA == false then self:updateView("lbl_Lights", "text", "<span class=\"section-title\"><font color=#0> Downlight Status:<b><font color=#f44336> Off </font></b>") end
            if downLightCA == true then self:updateView("lbl_Lights", "text", "<span class=\"section-title\"><font color=#0> Downlight Status:<b><font color=#6aa84f> On </font></b>") end
            if dimmerCA == "Off" then self:updateView("lbl_LEDRING", "text", "<span class=\"section-title\"><font color=#0> LEDring status:<b><font color=#f44336> Off </font></b>") end
            if dimmerCA == "Low" then self:updateView("lbl_LEDRING", "text", "<span class=\"section-title\"><font color=#0> LEDring Status:<b><font color=#6aa84f> Low </font></b>") end
            if dimmerCA == "Medium" then self:updateView("lbl_LEDRING", "text", "<span class=\"section-title\"><font color=#0> LEDring Status:<b><font color=#6aa84f> Medium </font></b>") end
            if dimmerCA == "High" then self:updateView("lbl_LEDRING", "text", "<span class=\"section-title\"><font color=#0> LEDring status:<b><font color=#6aa84f> High </font></b>") end
            self: GetChargerStatus()  
    end}
end

-- Get Charger Status
function QuickApp:GetChargerStatus()
    self:apiCallCA{method='GET',
                   path='/v4/chargepoints/'..idCA..'/connectors/1/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA},
    response=function(data)
            if data.mode == "Off" then self:updateView("lbl_Charger", "text", "<span class=\"section-title\"><b><font color=#000000>CHARGER Status:<b><font color=#f44336> Off</font></b>") end
            if data.mode == "On" then self:updateView("lbl_Charger", "text", "<span class=\"section-title\"><b><font color=#000000>CHARGER Status:<b><font color=#6aa84f> On </font></b>") end
            if data.mode == "Schema" then self:updateView("lbl_Charger", "text", "<span class=\"section-title\"><b><font color=#000000>CHARGER Status:<b><font color=#5794f2> Schema </font></b>") end
            self:GetOutletStatus()
    end}
end

-- Get Outlet Status
function QuickApp:GetOutletStatus()
    self:apiCallCA{method='GET',
                   path='/v4/chargepoints/'..idCA..'/connectors/2/settings',
                   headers = {["accept"] = "*/*", ["Authorization"] = "Bearer " ..tokenCA},
    response=function(data)
            if data.mode == "Off" then self:updateView("lbl_Outlet", "text", "<span class=\"section-title\"><b><font color=#000000>OUTLET Status:<b><font color=#f44336> Off</font></b>") end
            if data.mode == "On" then self:updateView("lbl_Outlet", "text", "<span class=\"section-title\"><b><font color=#000000>OUTLET Status:<b><font color=#6aa84f> On</font></b>") end
            if data.mode == "Schedule" then self:updateView("lbl_Outlet", "text", "<span class=\"section-title\"><b><font color=#000000>OUTLET Status:<b><font color=#5794f2> Schema </font></b>") end
    end}
end
-- -- -- -- -- -- -- CODE FOR DEVICE CONTROL -- -- -- -- -- -- -- -- --
-- -- -- -- -- -- -- -- -- -- END-- -- -- -- -- -- -- -- -- -- -- -- --



-- -- -- -- -- -- --** Initial start routine ** -- -- -- -- -- -- -- --
function QuickApp:onInit()
    QA=self
    QA:debug(QA.name, "- QAid: ", QA.id)

    -- Device variables
    local email = self:getVariable("email")
    local pwd = self:getVariable("pwd")
    local APIkey = self:getVariable("apiKey")

    -- Update labels.
    self:updateView("lbl_DevControl", "text", "<span class=\"section-title\"><b><font color=#cccc00>🚗🚙🚕🛻🚗DEVICE CONTROL🚗🛻🚕🚙🚗</font></b>")
    self:updateView("lbl_Info", "text", "<span class=\"section-title\"><b><font color=#cccc00>🚗🚙🚕🛻🚗 DEVICE INFO 🚗🛻🚕🚙🚗</font></b>")
           
    setTimeout(function() self:CAlogin(email,pwd,APIkey) end,0)

end

Guess I need to dig in deeper in the Atoms documentation and also look much more in depth into your different Apps and see if I can figure out how to start…

To get started with OAuth, Athom have a basic guide to get started

Logging in is the hard part, after that it is just defining API calls as required.
Most of my apps that use OAuth had to vary the method because the device manufactures have deviated from the specification, but most started via that guide.

In my case I do believe that the challenges are multiple… :slight_smile:

Thinking to create:

  • an stupid simple App, more or less empty.
  • Guess I need to add a “Device” in it to be able to add a Charger device to test with.
  • Then try to work out the OAuth2 thing to connect to Charge Amps.

So no Flow “cards” to start with, just an almost empty App with the possibility to add a Device.

But even that will be a challenge as my coding skills are limited and never touched a Node.js code before.

@Adrian_Rockall

Actually Charge Amps has a ”Swagger” to their REST API, not sure if this helps but to get the connectivity working I some how need to figure out how to do the following and perhaps the output in the Swagger can give some guidance what is needed:

  1. First I need to Login to Charge Amps REST API with:
  • Username
  • Password
  • API key

According to the Swagger the curl code for this is:

curl -X 'POST' \
  'https://eapi.charge.space/api/v4/auth/login' \
  -H 'accept: application/json' \
  -H 'apiKey: secretAPIkeyishere' \
  -H 'Content-Type: application/json' \
  -d '{
  "email": "user@domain.com",
  "password": "password"
}'

Once that is successful I will get a Token back and also a Refresh Token:

Response body
{
  "message": "Login succeeded",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZW5zQGJvcmdzdHJhbmQuc2UiLCJqdGk eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZW5zQGJvcmdzdHJhbmQuc2UiLCJqdGk eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZW5zQGJvcmdzdHJhbmQuc2UiLCJqdGk eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZW5zQGJvcmdzdHJhbmQuc2UiLCJqdGk. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZW5zQGJvcmdzdHJhbmQuc2UiLCJqdGk eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp",
  "user": {
    "id": "999999g9-1234-3abc-b5f6-99c6523d78f5",
    "firstName": "Firstname",
    "lastName": "Lastname",
    "email": "user@domain.com",
    "mobile": "1234567890",
    "rfidTags": null,
    "userStatus": "Valid"
  },
  "refreshToken": "bonqjoTNmBvYdv4ovSmrNXoiSs=bonqjoTNmBvYdv4ovSmrNXoiSs="
}

Once I get this far things forward will be as you say much more ”simple”, then it is more or less rather straight forward API calls to interact with the Charge Amps charger where you use the Token you have received in the API requests.

Also need to figure out how to refresh the Token on a reoccurring basis as it is only valid for 120minutes.

But guess I will struggle a lot with the first part… the Login to Charge Amps REST API… Seams to be a challenging task to sort out how to get it working… :slight_smile:

In the Fibaro HC3 QuickAPP in LUA code this was done with this function:

-- Login to the ChargeAmps API.
function QuickApp:CAlogin(email,pwd,APIkey)
    self:apiCallCA{ignoreTokenCheck=true, method='POST',
                   path='/v4/auth/login',
                   headers={["apiKey"] = APIkey, ["Content-Type"] = "application/json", ["accept"] = "*/*"},
                   data={email=email,password=pwd},
        response=function(data)
            tokenCA = data.token
            refreshTokenCA = data.refreshToken
            setInterval(function() self:renewToken() end,1000*7080)
            self:GetOwned()
        end,
        error=function(msg) self:error("Failed login",msg)
        end}
end

And this API Call function (called from the login function above):

-- Charge Amps API Call function
function QuickApp:apiCallCA(args)
    if (not args.ignoreTokenCheck) and tokenCA==nil then error("Not logged in") end
    local method = args.method
    local path = args.path
    local data = args.data
    local response = args.response
    local err = args.error
    local headers = {
                   ["accept"] = "application/json", 
                   ["Authorization"] = type(tokenCA)=='string' and ("Bearer " ..tokenCA) or nil
                   }
    for n,v in pairs(args.headers or {}) do headers[n]=v end
    http = net.HTTPClient()
    http:request("https://eapi.charge.space/api"..path, 
        {
        options = {
                 method = method or "GET",
                 headers = headers,
                 data = data and json.encode(data) or nil
                 },
        success = function(resp)
            local data = json.decode(resp.data)
            if response then response(data) end
        end,
        error = function(msg) 
            self:error(path,msg) 
            if err then err(msg) end
        end})
end

@Adrian_Rockall

You have so many Apps… I understand that API requests can differ a lot between implementation, but which of your apps would you say are most similar to what I am trying to build?

Meaning which of your app does do this:

  1. Login/Authenticate with UserName, Password & APIkey
  2. When successfully login receives a Token that is used in all other API requests
  3. Renewal of Token on a regular basis.

Not aiming to steal your code… :slight_smile: But need some guidance to learn how to do this, and a good way to learn is to look at a working implementation.

Off the top of my head I would say Tahoma (cloud login, but complex due to dual mode), LightWave (my first app, so a bit quirky, but has been extremely reliable so not touched it) and LinkTap. I don’t have access to my code at the moment, so this is just from memory, but I think LinkTap might be the easiest example.

@Adrian_Rockall

Thanks for the tip! Did take a quick look at the LinkTap app but as far as I can see it does not use OAuth2?

Correct, but the login method you referred to is not OAUTH2 either. OAUTH2 login is where you would open a login page on the provider’s website to login and it returns the tokens to a predefined url.

You mentioned that you would call an API method, passing in a username and password plus an API token and the access token, etc is returned. That is how the LinkTap app works.

@Adrian_Rockall

You are completely correct! My misstake thinking this was the OAuth2 method :upside_down_face:

Then I will definitely dig into your LinkTap app and see how this could be done! Need scrap all what I have tested with OAuth2 and instead look deeper into how you do it in the LinkTap app and see how I can implement this.

@Adrian_Rockall

Solved the connectivity part in an unexpected way… Started to looking to your LinkTap code and had started to understand how it worked.

But decided to give an alternative route a try and was rather amazed that it worked.

I went into ChatGPT ( :slight_smile:) and asked it to build an Homey App based on the Fibaro HC3 QuickAPP code in LUA that I developed some time ago. Had to correct a few minor things but surprisingly it work as a charm including the renewal of Token on a given interval.

So now I dig more into how to actually build an entire Homey APP around this connectivity.

1 Like

Great. ChatGpt is pretty amazing.
I have just started to GitHub CodePilot and I am amazed at how well it works.

Hi Borgen! I’m also using a HP23 and a Charge Amps Halo, so I will gladly help you with any testing :slight_smile:

@Jimmy_Hurtig, thanks!

There is now an early stage App submitted to Homey for approval, hope that it will be available soon for testing.

More details are here: [APP][Pro] ChargeAmps HALO

3 Likes

Same as Jimmy, I’ll happily support with testing and ideas! I’ve built a simple ChargeAmps “app” based on homey script and"device capabilities app". It works but a proper app is of course what’s needed.

App is released and available in the Homey App Store.

Testing and suggestions for improvements are welcome

1 Like