Skip to content
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

Propositions for enchancements #5

Open
yawor opened this issue Jun 9, 2023 · 10 comments
Open

Propositions for enchancements #5

yawor opened this issue Jun 9, 2023 · 10 comments

Comments

@yawor
Copy link
Contributor

yawor commented Jun 9, 2023

Hi,

The script works great, but there's a lot of space for making it better.

Right now it can't be used as a library in other software, for example as a library used by HA integration. This is because all code is executed as soon as the file is imported. For this to work as a library, the main logic should also be inside a function and it should be called in a guarding condition like this:

if __name__ == "__main__":
    main()

if the main logic is in the main() function.

For this to work great as a library (especially if used in the HA integration) the requests library should be replaced by some async http library like aiohttp or httpx. HA already uses httpx and has it installed, so I'd opt for using this one also.

RIght now the script doesn't use refresh token, which is actually really helpful. I'd change whole authentication part of the script. First of all I'd separate authentication from opening the doors.
The user would first use auth command. Providing username and password in the command line could left as an option, but I'd use prompts if either one or both of them is not provided. So user would just call auth and the script would first as for username and then for password (in safe way, hiding the input). The auth would then save both access token and refresh token, together with max age (expiry timestamp could also be calculated right away ans stored).
The door opening would not accept username and password anymore. Instead it would need to check if the access token is almost expired (for example if it expires in 2 days or less, which is half of the max age for the token) - if yes, then before opening the doors, it would use refresh_token to refresh access token by POSTing grant_type=refresh_token&refresh_token=<current_refresh_token> to /oauth/token. This is a correct way of using OAuth2 and I've tested it using curl and this works correctly on Fermax server.
There could also be a command to only check if access token expires shortly and refresh it if it's needed - this could be run from crontab to always keep the access token fresh.

The last thing is to properly package this script as a library. I propose using Poetry as a packaging tool.

@marcosav
Copy link
Owner

marcosav commented Jun 9, 2023

Hi @yawor, thank you very much for your feedback, I really appreciate it!, every single thing you've pointed out it's quite interesting, and I completely agree, sadly I don't have much free time to maintain this script 😢. If you are interested in contributing to the project with any small improvement it would be awesome.

@yawor
Copy link
Contributor Author

yawor commented Jun 9, 2023

I know that's a lot of work and I'm fully prepared to work on these points. To be honest, I've already started :).

@yawor
Copy link
Contributor Author

yawor commented Jun 9, 2023

BTW @marcosav, would you be able to share the original HTTP traffic you've captured from the Blue app (of course with your credentials or deviceId stripped)? Right now, when calling directed-opendoor endpoint to open the door the server returns a message in Spanish. When I open the door from the Blue app, I get a message in Polish that the door is opened. I also get appropriate message when I try to open a door that doesn't exist.
I think that the message displayed in the app is actually sent by the server and not embedded in the app itself. I've decompiled the Blue app and there are no such messages there.
This means it's possible that the server can return localized messages instead only in Spanish. I've tried sending Accept-Language header set to pl_PL, but I still get a message in Spanish. Maybe there is some other parameter passed from the app to the server.

For some reason I can't capture any traffic from the Blue app using mitmproxy. The app probably uses certificate pinning, which causes it not to accept mitmproxy generated certificate (I've had mitmproxy CA certificate installed as trusted during the tests).

@marcosav
Copy link
Owner

Of course!

As soon as I open the Blue app the following executes:

{
  "email": "myemail@domain.com",
  "locale": "EN",
  "acceptSharing": false,
  "acceptPrivacy": true,
  "enabled": true,
  "createdAt": "2023-03-25T10:19:24.063+00:00",
  "country": "es",
  "city": "City",
  "area": "Norte",
  "zone": "Portugal and Spain",
  "subzone": "Spain",
  "pin": null,
  "pinDate": null,
  "uniqueSession": false,
  "provider": null,
  "name": null
}
[
  {
    "id": "A UUID",
    "deviceId": "MyDeviceID",
    "tag": "Portal",
    "status": "PAIRED",
    "updatedAt": 1679740105258,
    "createdAt": 1679740067990,
    "appBuild": 599,
    "appVersion": "3.2.1",
    "phoneModel": "MyAndroidPhoneModel",
    "phoneOS": "MyAndroidPhoneOS",
    "home": null,
    "address": null,
    "accessDoorMap": {
      "ZERO": {
        "title": "",
        "accessId": {
          "block": 200,
          "subblock": -1,
          "number": 0
        },
        "visible": true
      },
      "ONE": {
        "title": "",
        "accessId": {
          "block": 200,
          "subblock": -1,
          "number": 1
        },
        "visible": false
      },
      "GENERAL": {
        "title": "",
        "accessId": {
          "block": 100,
          "subblock": -1,
          "number": 0
        },
        "visible": false
      }
    },
    "master": true
  }
]
{
  "deviceId": "MyDeviceID",
  "connectionState": "Connected",
  "status": "ACTIVATED",
  "primaryThumbprint": null,
  "installationId": "",
  "family": "MONITOR",
  "type": "VEO-XS",
  "subtype": "WIFI",
  "numBlock": 0,
  "numSubblock": 0,
  "unitNumber": 2,
  "connectable": true,
  "iccid": "",
  "divertService": "blueStream",
  "photocaller": true,
  "wirelessSignal": 4,
  "blueStream": true,
  "phone": false,
  "panelOrEdibox": false,
  "monitor": true,
  "terminal": true
}
[
  "AccessName",
  "AutoOn",
  "CallDivert",
  "CallRegistry",
  "ChangeVideoSource",
  "CheckInformation",
  "DND",
  "Doormatic",
  "EventRegistry",
  "F1",
  "F1Options",
  "Geo",
  "Guard",
  "Guest5",
  "ManageCallDivert",
  "OpenDoor",
  "Photocaller",
  "Ringtone",
  "SessionsUnlimited",
  "TZ"
]
{
  "number": 0,
  "block": 200,
  "subblock": -1
}

Response:

la puerta abierta

Also, this may be useful, when I press the video button:

{
  "directedToBluestream": "MyAppToken",
  "deviceID": "MyDeviceID"
}

Response:

{
  "reason": "call_starting",
  "divertService": "blueStream",
  "code": 1,
  "additional_info": {
    "technology": "duox",
    "remote": {
      "number": 0,
      "address": "00 F0 A0",
      "block": 0,
      "type": "panel",
      "subblock": -1
    },
    "local": {
      "number": 2,
      "address": "00 00 02",
      "subtype": "monitor",
      "block": 0,
      "type": "terminal",
      "subblock": 0
    }
  },
  "description": "Auto on is starting",
  "directedTo": "MyAppToken"
}
{
  "timestamp": "2023-06-11T15:35:40.368+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/api/v1/message/ack"
}
119:0{"sid":"A20characterAlphanumericId","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":100000,"maxPayload":1000000}
2:40
1:2

Response:

ok
1:3
7465:430[{"result":{"iceServers":[{"credential":"pulpoconcachelos","credentialType":"password","urls":["turn:coturn-pro-west-europe.westeurope.cloudapp.azure.com:443?transport=tcp"],"username":"fermaxturn"}],"producerIdVideo":"487884e7-1615-40c0-9b93-205dd7822f8c","producerIdAudio":"ab570315-8619-41b4-9a1c-ac2ab9fd781a","recvTransportVideo":{"id":"eee97d17-b6db-4be7-ab0b-dd69cdaa141e","dtlsParameters":{"fingerprints":[{"algorithm":"sha-256","value":"2F:0C:C1:1F:5C:90:A4:36:FC:61:27:DF:8B:44:D8:33:47:9E:D1:FC:17:3A:E6:BA:34:E9:CE:53:C2:E4:D1:EA"},{"algorithm":"sha-384","value":"67:10:D2:45:2A:57:AC:06:7A:5F:B7:2D:7B:FD:0A:E3:9D:C5:D2:54:58:A0:6D:B9:31:A9:56:CC:5E:CC:CF:2C:97:CF:D9:1A:1F:5C:12:B2:6F:49:B2:14:8F:14:45:22"},{"algorithm":"sha-1","value":"38:FE:94:EB:D1:12:87:D0:58:58:72:21:0E:AE:93:E4:AA:27:6C:1E"},{"algorithm":"sha-512","value":"BF:CF:47:B6:AF:9E:C5:F1:33:4C:97:98:DD:D3:94:F6:E4:A9:1A:2D:45:87:BD:08:06:68:D6:A6:94:4F:E0:13:B5:FE:E3:23:06:3E:89:99:52:45:24:69:93:DD:8A:77:C6:B7:BC:6F:92:D7:72:13:66:F2:7C:05:DB:DF:41:73"},{"algorithm":"sha-224","value":"95:B0:25:D3:11:9A:96:C0:EC:80:51:C6:E6:21:55:47:8B:EE:92:F9:B4:62:14:48:A0:44:49:62"}],"role":"auto"},"iceCandidates":[{"foundation":"udpcandidate","ip":"20.126.206.57","port":58759,"priority":1076302079,"protocol":"udp","type":"host"},{"foundation":"udpcandidate","ip":"2603:1020:201:10::52","port":41034,"priority":1076276479,"protocol":"udp","type":"host"}],"iceParameters":{"iceLite":true,"password":"64d71r2bohos0318bdaoi4rcxmd9pged","usernameFragment":"4ubxz5g9u7aee5f6tzoygixe74jn32gv"}},"routerRtpCapabilities":{"codecs":[{"kind":"audio","mimeType":"audio/PCMA","preferredPayloadType":8,"clockRate":8000,"rtcpFeedback":[{"type":"transport-cc","parameter":""}],"channels":1,"parameters":{}},{"kind":"video","mimeType":"video/H264","clockRate":90000,"parameters":{"level-asymmetry-allowed":1,"packetization-mode":1,"profile-level-id":"4d0032","x-google-start-bitrate":1000},"rtcpFeedback":[{"type":"nack","parameter":""},{"type":"nack","parameter":"pli"},{"type":"ccm","parameter":"fir"},{"type":"goog-remb","parameter":""},{"type":"transport-cc","parameter":""}],"preferredPayloadType":100},{"kind":"video","mimeType":"video/rtx","preferredPayloadType":101,"clockRate":90000,"parameters":{"apt":100},"rtcpFeedback":[]},{"kind":"video","mimeType":"video/H264","clockRate":90000,"parameters":{"level-asymmetry-allowed":1,"packetization-mode":1,"profile-level-id":"42e01f","x-google-start-bitrate":1000},"rtcpFeedback":[{"type":"nack","parameter":""},{"type":"nack","parameter":"pli"},{"type":"ccm","parameter":"fir"},{"type":"goog-remb","parameter":""},{"type":"transport-cc","parameter":""}],"preferredPayloadType":102},{"kind":"video","mimeType":"video/rtx","preferredPayloadType":103,"clockRate":90000,"parameters":{"apt":102},"rtcpFeedback":[]}],"headerExtensions":[{"kind":"audio","uri":"urn:ietf:params:rtp-hdrext:sdes:mid","preferredId":1,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:sdes:mid","preferredId":1,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id","preferredId":2,"preferredEncrypt":false,"direction":"recvonly"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id","preferredId":3,"preferredEncrypt":false,"direction":"recvonly"},{"kind":"audio","uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time","preferredId":4,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time","preferredId":4,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"audio","uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01","preferredId":5,"preferredEncrypt":false,"direction":"recvonly"},{"kind":"video","uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01","preferredId":5,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07","preferredId":6,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:framemarking","preferredId":7,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"audio","uri":"urn:ietf:params:rtp-hdrext:ssrc-audio-level","preferredId":10,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:3gpp:video-orientation","preferredId":11,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"urn:ietf:params:rtp-hdrext:toffset","preferredId":12,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"video","uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time","preferredId":13,"preferredEncrypt":false,"direction":"sendrecv"},{"kind":"audio","uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time","preferredId":13,"preferredEncrypt":false,"direction":"sendrecv"}]},"sendTransport":{"id":"e7af7ab4-d226-4a46-8e72-83f25618e911","dtlsParameters":{"fingerprints":[{"algorithm":"sha-256","value":"2F:0C:C1:1F:5C:90:A4:36:FC:61:27:DF:8B:44:D8:33:47:9E:D1:FC:17:3A:E6:BA:34:E9:CE:53:C2:E4:D1:EA"},{"algorithm":"sha-384","value":"67:10:D2:45:2A:57:AC:06:7A:5F:B7:2D:7B:FD:0A:E3:9D:C5:D2:54:58:A0:6D:B9:31:A9:56:CC:5E:CC:CF:2C:97:CF:D9:1A:1F:5C:12:B2:6F:49:B2:14:8F:14:45:22"},{"algorithm":"sha-1","value":"38:FE:94:EB:D1:12:87:D0:58:58:72:21:0E:AE:93:E4:AA:27:6C:1E"},{"algorithm":"sha-512","value":"BF:CF:47:B6:AF:9E:C5:F1:33:4C:97:98:DD:D3:94:F6:E4:A9:1A:2D:45:87:BD:08:06:68:D6:A6:94:4F:E0:13:B5:FE:E3:23:06:3E:89:99:52:45:24:69:93:DD:8A:77:C6:B7:BC:6F:92:D7:72:13:66:F2:7C:05:DB:DF:41:73"},{"algorithm":"sha-224","value":"95:B0:25:D3:11:9A:96:C0:EC:80:51:C6:E6:21:55:47:8B:EE:92:F9:B4:62:14:48:A0:44:49:62"}],"role":"auto"},"iceCandidates":[{"foundation":"udpcandidate","ip":"20.126.206.57","port":48517,"priority":1076302079,"protocol":"udp","type":"host"},{"foundation":"udpcandidate","ip":"2603:1020:201:10::52","port":31679,"priority":1076276479,"protocol":"udp","type":"host"}],"iceParameters":{"iceLite":true,"password":"1z13ibcx888z5ovx242sf4xq5xdgup8y","usernameFragment":"fhjpkd6rmqa1zylrwe4yl67gnznegnys"}},"recvTransportAudio":{"id":"c1f80510-849c-4134-90a4-644ad1b9068c","dtlsParameters":{"fingerprints":[{"algorithm":"sha-256","value":"2F:0C:C1:1F:5C:90:A4:36:FC:61:27:DF:8B:44:D8:33:47:9E:D1:FC:17:3A:E6:BA:34:E9:CE:53:C2:E4:D1:EA"},{"algorithm":"sha-384","value":"67:10:D2:45:2A:57:AC:06:7A:5F:B7:2D:7B:FD:0A:E3:9D:C5:D2:54:58:A0:6D:B9:31:A9:56:CC:5E:CC:CF:2C:97:CF:D9:1A:1F:5C:12:B2:6F:49:B2:14:8F:14:45:22"},{"algorithm":"sha-1","value":"38:FE:94:EB:D1:12:87:D0:58:58:72:21:0E:AE:93:E4:AA:27:6C:1E"},{"algorithm":"sha-512","value":"BF:CF:47:B6:AF:9E:C5:F1:33:4C:97:98:DD:D3:94:F6:E4:A9:1A:2D:45:87:BD:08:06:68:D6:A6:94:4F:E0:13:B5:FE:E3:23:06:3E:89:99:52:45:24:69:93:DD:8A:77:C6:B7:BC:6F:92:D7:72:13:66:F2:7C:05:DB:DF:41:73"},{"algorithm":"sha-224","value":"95:B0:25:D3:11:9A:96:C0:EC:80:51:C6:E6:21:55:47:8B:EE:92:F9:B4:62:14:48:A0:44:49:62"}],"role":"auto"},"iceCandidates":[{"foundation":"udpcandidate","ip":"20.126.206.57","port":57220,"priority":1076302079,"protocol":"udp","type":"host"},{"foundation":"udpcandidate","ip":"2603:1020:201:10::52","port":31794,"priority":1076276479,"protocol":"udp","type":"host"}],"iceParameters":{"iceLite":true,"password":"9zuvg180gjp8vle0m0f4lmhldwyp02fx","usernameFragment":"n9k96ru2i93i9j8asel2fsfznigrqk93"}}}}]
750:420["join_call",{"protocolVersion":"0.8.2","appToken":"MyAppToken","fermaxOauthToken":"AuthToken","roomId":"MyDeviceID_1686495975411"}]

Response:

ok
1306:431[{"result":{"id":"a41d6b20-345f-48d9-88db-00e46d80e915","kind":"video","producerId":"487884e7-1615-40c0-9b93-205dd7822f8c","rtpParameters":{"codecs":[{"mimeType":"video/H264","payloadType":102,"clockRate":90000,"parameters":{"level-asymmetry-allowed":1,"packetization-mode":1,"profile-level-id":"42e01f"},"rtcpFeedback":[{"type":"transport-cc","parameter":""},{"parameter":"fir","type":"ccm"},{"parameter":"","type":"nack"},{"parameter":"pli","type":"nack"}]},{"mimeType":"video/rtx","payloadType":103,"clockRate":90000,"parameters":{"apt":102},"rtcpFeedback":[]}],"headerExtensions":[{"uri":"urn:ietf:params:rtp-hdrext:sdes:mid","id":1,"encrypt":false,"parameters":{}},{"uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time","id":4,"encrypt":false,"parameters":{}},{"uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01","id":5,"encrypt":false,"parameters":{}},{"uri":"http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07","id":6,"encrypt":false,"parameters":{}},{"uri":"urn:3gpp:video-orientation","id":11,"encrypt":false,"parameters":{}},{"uri":"urn:ietf:params:rtp-hdrext:toffset","id":12,"encrypt":false,"parameters":{}}],"encodings":[{"ssrc":893115811,"rtx":{"ssrc":893115812}}],"rtcp":{"cname":"5c46dbd2","reducedSize":true,"mux":true},"mid":"0"}}}]
2067:421["transport_consume",{"producerId":"487884e7-1615-40c0-9b93-205dd7822f8c","rtpCapabilities":{"headerExtensions":[{"uri":"urn:ietf:params:rtp-hdrext:sdes:mid","preferredId":1,"kind":"audio","direction":"sendrecv","preferredEncrypt":false},{"direction":"sendrecv","preferredEncrypt":false,"uri":"urn:ietf:params:rtp-hdrext:sdes:mid","preferredId":1,"kind":"video"},{"preferredId":4,"direction":"sendrecv","preferredEncrypt":false,"uri":"http:\/\/www.webrtc.org\/experiments\/rtp-hdrext\/abs-send-time","kind":"audio"},{"direction":"sendrecv","uri":"http:\/\/www.webrtc.org\/experiments\/rtp-hdrext\/abs-send-time","preferredEncrypt":false,"kind":"video","preferredId":4},{"preferredId":5,"uri":"http:\/\/www.ietf.org\/id\/draft-holmer-rmcat-transport-wide-cc-extensions-01","preferredEncrypt":false,"kind":"video","direction":"sendrecv"},{"preferredEncrypt":false,"preferredId":6,"direction":"sendrecv","kind":"video","uri":"http:\/\/tools.ietf.org\/html\/draft-ietf-avtext-framemarking-07"},{"uri":"urn:ietf:params:rtp-hdrext:ssrc-audio-level","preferredId":10,"kind":"audio","direction":"sendrecv","preferredEncrypt":false},{"uri":"urn:3gpp:video-orientation","direction":"sendrecv","preferredId":11,"kind":"video","preferredEncrypt":false},{"uri":"urn:ietf:params:rtp-hdrext:toffset","kind":"video","preferredId":12,"direction":"sendrecv","preferredEncrypt":false}],"codecs":[{"kind":"audio","channels":1,"parameters":{},"mimeType":"audio\/PCMA","preferredPayloadType":8,"rtcpFeedback":[],"clockRate":8000},{"mimeType":"video\/H264","parameters":{"profile-level-id":"42e01f","packetization-mode":1,"level-asymmetry-allowed":1},"rtcpFeedback":[{"type":"goog-remb","parameter":""},{"type":"transport-cc","parameter":""},{"parameter":"fir","type":"ccm"},{"parameter":"","type":"nack"},{"parameter":"pli","type":"nack"}],"clockRate":90000,"preferredPayloadType":102,"kind":"video"},{"rtcpFeedback":[],"mimeType":"video\/rtx","parameters":{"apt":102},"clockRate":90000,"preferredPayloadType":103,"kind":"video"}]},"transportId":"eee97d17-b6db-4be7-ab0b-dd69cdaa141e"}]

Response:

ok
5:432[]
261:422["transport_connect",{"dtlsParameters":{"role":"client","fingerprints":[{"value":"AA:AA:AA:AA:AA:AA:C1:0C:44:29:EF:85:1B:A5:AA:29:AA:7F:73:C6:F7:AA:C5:2F:F8:E3:AA:AA:AA:AA:AA:AA","algorithm":"sha-256"}]},"transportId":"eee97d17-b6db-4be7-ab0b-dd69cdaa141e"}]

Response:

ok
1:6

Now video is loaded, then I pressed the green button (call button) but no more HTTP requests where made. Finally, when I ended the call (hang button), /device and /services endpoints are called as before.

Hope this is usefull!

@yawor
Copy link
Contributor Author

yawor commented Jun 12, 2023

Thanks for all the request/response bodies. It may be helpful in the future.

I'm not ready yet to create pull request, but I've pushed everything I've done up to this moment to my fork here:
https://github.com/yawor/fermax-blue-intercom/tree/rewrite

README file definitely needs a rewrite, as whole command line interface has changed.

When I'm done and this is merged, it would be great to publish this on pypi. Maybe even integrate github actions for automatic build and publish.

@cvc90
Copy link

cvc90 commented Sep 11, 2023

Hi, would it be possible to add some of the following options?

  • Call History
  • Last event
  • Last picture
  • Take photo

UPDATE: I made a fork of the project in which it was updated with new functions, I leave the link in case you want to take a look. Link to fork

@bvis
Copy link

bvis commented Dec 27, 2023

Hi, thanks for your work!

Any improvements on this? @yawor your work was amazing, didn't you have time for finishing it?

@soloam
Copy link

soloam commented Apr 17, 2024

I found this when looking for fermax on github! Did you abandoned the project @yawor? Any advancements?

@simposiummm
Copy link

@yawor thanks for your code? Can you confirm if the lates version is on this repository?

@yawor
Copy link
Contributor Author

yawor commented Nov 13, 2024

Hi,

I'm sorry everyone for the lack of updates. I've had a lot of work recently and I've had not much time for any side projects. Regarding my changes, everything I've done for now is pushed to my repo, but I didn't have time to prepare a proper documentation. That's why I didn't do any pull requests to this original repository.

The current status of my rewrite:

  • It's now fully asychronous. It's based on AnyIO, but tested only on asyncio, so I have no idea if it's going to work on Trio backend.
  • It can be used as both a command line tool and a library to be used in some other software (for example as a library for custom Home Assistant integration).
  • It does't store username and password anywhere. It has an auth command which takes username and password, but uses it only for authentication. What is stored is an access token returned by the Fermax API. It's long lived. I've just tested and my access token from over a year ago still worked today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants