xbezdick / amotionatrea

GNU General Public License v3.0
1 stars 2 forks source link

Unauthorized: Token not found #1

Closed alexelite closed 2 months ago

alexelite commented 3 months ago

Hello Lukáš Bezdička

Did you manage to get this working? I only get "Unauthorized: Token not found" when I try to login. I tried with curl also, with no success. Login returns no error, and result contains a string, but it is not accepted as token. Do you have some documentation that you can share?

Thank you very much

Alex

xbezdick commented 3 months ago
 curl -H "Content-Type: application/json" -vvvv -X POST 172.30.0.11/api/login -d '{"username":"test","password":"P4ssword"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 172.30.0.11:80...
* Connected to 172.30.0.11 (172.30.0.11) port 80
> POST /api/login HTTP/1.1
> Host: 172.30.0.11
> User-Agent: curl/8.6.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 48
> 
< HTTP/1.1 200 OK
< Server: 1.4.4
< Date: Sun, 09 Jun 2024 10:44:48 GMT
< Access-Control-Allow-Origin: *
< Content-Length: 53
< Content-Type: application/json
< Connection: keep-alive
< Last-Modified: Sun, 09 Jun 2024 10:44:48 GMT
< Pragma: no-cache
< Cache-Control: no-store
< Expires: Sun, 09 Jun 2024 10:44:48 GMT
< Access-Control-Allow-Headers: *
< Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
< 
* Connection #0 to host 172.30.0.11 left intact
{"code":"OK","error":null,"result":"ATCwwTOKEN"}

What are you getting if you curl /openapi.yaml endpoint?

alexelite commented 3 months ago
curl -H "Content-Type: application/json" -vvvv -X POST 192.168.1.118/api/login -d '{"username":"ha","password":"haos"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Expire in 0 ms for 6 (transfer 0x55809000afb0)
*   Trying 192.168.1.118...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55809000afb0)
* Connected to 192.168.1.118 (192.168.1.118) port 80 (#0)
> POST /api/login HTTP/1.1
> Host: 192.168.1.118
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 35
>
* upload completely sent off: 35 out of 35 bytes
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 53
< Connection: close
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Headers: *
< Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
<
* Closing connection 0
{"code":"OK","error":null,"result":"ATCvxpjk8jnsbxa"}

I get a token similar to yours. If I use it after, I get INVALID_TOKEN.


curl -vvvv -H "X-ATC-TOKEN: ATCvxpjk8jnsbxa"   --header "Content-Type: application/json"   --request POST   --data '{"variables": {"fan_power_req": 10}}'   http://192.168.1.118/api/control
Note: Unnecessary use of -X or --request, POST is already inferred.
* Expire in 0 ms for 6 (transfer 0x55e6e2105fb0)
*   Trying 192.168.1.118...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55e6e2105fb0)
* Connected to 192.168.1.118 (192.168.1.118) port 80 (#0)
> POST /api/control HTTP/1.1
> Host: 192.168.1.118
> User-Agent: curl/7.64.0
> Accept: */*
> X-ATC-TOKEN: ATCvxpjk8jnsbxa
> Content-Type: application/json
> Content-Length: 36
>
* upload completely sent off: 36 out of 36 bytes
< HTTP/1.1 401 Unauthorized
< Content-Type: application/json
< Content-Length: 78
< Connection: close
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Headers: *
< Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
<
* Closing connection 0
{"code":"INVALID_TOKEN","error":"Unauthorized: Token not found","result":null}```
xbezdick commented 3 months ago

let's try calling different endpoint -

curl -vvvv -H "X-ATC-TOKEN: ATCTOKEN" --header "Content-Type: application/json" http://172.30.0.11/api/users

I get: {"code":"OK","error":null,"result":[{"code":false,"devices":["unit"],"email":"","localisation":null,"name":"Admin","permissions":["controll","remote","scheduler","admin"],"username":"admin"},{"code":true,"devices":["unit"],"email":"","localisation":"cs","name":"test","permissions":["controll","scheduler","remote","admin"],"username":"test"}]}

My guess is permissions of HA user?

alexelite commented 3 months ago

image

The user has all the permissions available.

➜  ~ curl --header "Content-Type: application/json" \                                                                   
  --request POST \
  --data '{"username":"ha","password":"TXcGDg5R8Rw63ei"}' \
  http://192.168.1.170/api/login

{"code":"OK","error":null,"result":"ATCi5ykir1cgjm8"}#                                                                                                           
➜  ~ curl -vvvv -H "X-ATC-TOKEN: ATCi5ykir1cgjm8" --header "Content-Type: application/json" http://192.168.1.170/api/users
*   Trying 192.168.1.170:80...
* Connected to 192.168.1.170 (192.168.1.170) port 80
> GET /api/users HTTP/1.1
> Host: 192.168.1.170
> User-Agent: curl/8.7.1
> Accept: */*
> X-ATC-TOKEN: ATCi5ykir1cgjm8
> Content-Type: application/json
> 
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< Content-Type: application/json
< Content-Length: 78
< Connection: close
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Headers: *
< Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
< 
* Closing connection
{"code":"INVALID_TOKEN","error":"Unauthorized: Token not found","result":null}# 

Can you please show me your response to "api/version" endpoint? Mine says something about a "REMOTE_API":null

➜  ~ curl --header "Content-Type: application/json" http://192.168.1.170/api/version 
{"code":"OK","error":null,"result":{"COMPACT":"Compact_M v0.5.4\r\n","CONTROLLER":null,"CONTROL_ADMIN":null,"GATEWAY":{"build":"28.05.2024","githash":"ced6810","version":"aMCE-v2.2.1"},"MAPPER":null,"REMOTE_API":null}}#  

Thank you very much for your help.

xbezdick commented 3 months ago

{ "code": "OK", "error": null, "result": { "000-00-00-000-0000015": "ATREA BUS, v0.4.0, MODBUS, A=15\n", "BUS": "ATREA BUS, v0.4.0, APR_BUS\n", "BUS_EXT": "ATREA BUS, v0.4.0, APR_DRV, A=12\n", "BUS_INT": "ATREA BUS, v0.4.0, APR_DRV, A=11\n", "COMPACT": { "version": "Compact L" }, "CONTROLLER": { "build": "28.05.2024", "githash": "ced6810", "library_githash": "ced6810", "library_version": "regulator_devel-v2.2.1", "version": "ATC-v2.2.1" }, "CONTROL_ADMIN": { "build": "28.05.2024", "githash": "ced6810", "version": "ATC-v2.2.1" }, "GATEWAY": { "build": "28.05.2024", "githash": "ced6810", "version": "ATC-v2.2.1" }, "MAPPER": { "build": "28.05.2024", "githash": "ced6810", "version": "ATC-v2.2.1" } } } I had for a while modbus on but ended up turning it off. Plus I have it disconnected from the cloud. Versions seem to be same but I'm tempted to ask you to share http://192.168.1.170/openapi.yaml

alexelite commented 3 months ago
curl http://192.168.1.170/openapi.yaml                                  
This URI does not exist# 

My board is aM-CE. I tested on multiple EC5 370 and 570 units. Same results.

curl http"://192.168.1.170/api/discovery
{"code":"OK",
   "error":null,
   "result":{
      "activation_status":"READY",
      "board_number":"",
      "board_type":"CE",
      "brand":"atrea.cz",
      "cloud":{
         "enable":true,
         "link":"https://amotion.cloud",
         "support":true
      },
      "commissioned":false,
      "initialized":true,
      "localisation":"en",
      "name":"CORP-D  570 EC5.aM-CE",
      "port":80,
      "production_number":"",
      "service_name":"",
      "type":"DUPLEX 570 EC5.aM-CE",
      "version":"aMCE-v2.2.1"
   }
}
xbezdick commented 3 months ago

So when I curl /api it redirects me to the openapi.yaml. It seems the endpoints are the same just they probably refactored the auth token. We could try digging around the api or tinker with the auth. For example for the webUI it uses token in cookie.

This is what I get - https://pastebin.com/aic48LnW

alexelite commented 3 months ago

/api gives me a list of commands, posted below. Thank you again for your help and info.

my web ui uses /api/ws endpoint and a header record "Cookie: token=ATCxxx" I tried to using that token, but without any success.

{
   "code":"OK",
   "error":null,
   "result":[
      "",
      "backup",
      "backup_types",
      "backups",
      "clients",
      "cloud",
      "cloud/set",
      "commission",
      "commission/set",
      "config",
      "control",
      "control_admin/config/activate_calendar",
      "control_admin/config/activate_scene",
      "control_admin/config/apply_calendar",
      "control_admin/config/disposable_plan/get",
      "control_admin/config/disposable_plan/set",
      "control_admin/config/duplicate_calendar_day",
      "control_admin/config/edit_calendar",
      "control_admin/config/enable_trigger",
      "control_admin/config/enable_trigger_function",
      "control_admin/config/get_calendar",
      "control_admin/config/get_calendars",
      "control_admin/config/get_scenes",
      "control_admin/config/get_scenes_template",
      "control_admin/config/get_scheduler",
      "control_admin/config/get_scheduler_template",
      "control_admin/config/get_trigger_functions",
      "control_admin/config/get_trigger_input_types",
      "control_admin/config/get_trigger_inputs",
      "control_admin/config/get_triggered_variable_list",
      "control_admin/config/get_triggers",
      "control_admin/config/get_triggers_template",
      "control_admin/config/moments/get",
      "control_admin/config/moments/reset/filter",
      "control_admin/config/moments/reset/uvlamp",
      "control_admin/config/moments/set",
      "control_admin/config/remove_calendar",
      "control_admin/config/scenes_reset",
      "control_admin/config/set_calendar",
      "control_admin/config/set_calendar_plan",
      "control_admin/config/set_holidays",
      "control_admin/config/set_scene",
      "control_admin/config/set_scenes",
      "control_admin/config/set_scheduler",
      "control_admin/config/set_scheduler_plan_item",
      "control_admin/config/set_trigger",
      "control_admin/config/set_trigger_function",
      "control_admin/config/set_triggers",
      "control_panel",
      "custom/config",
      "custom/config/activate",
      "custom/config/set",
      "discovery",
      "get_triggers_report",
      "modbus",
      "modbus/set",
      "modbus_info",
      "network_manager",
      "network_manager/get",
      "network_manager/reset",
      "network_manager/set",
      "network_manager/status",
      "ping",
      "reboot",
      "states/logs",
      "states/reset",
      "support",
      "time",
      "time/set",
      "time/timezones",
      "ui_control_scheme",
      "ui_control_scheme/all",
      "ui_diagram_data",
      "ui_diagram_scheme",
      "ui_info",
      "ui_info_scheme",
      "unit/set",
      "update",
      "update/check",
      "update/set",
      "update/start",
      "uptime",
      "user",
      "user/set",
      "user/storage",
      "user/storage/clear",
      "user_config_get",
      "user_devices",
      "user_devices/set",
      "users",
      "users/create",
      "version"
   ]
}
alexelite commented 3 months ago

can you please share your /api/discovery endpoint?

xbezdick commented 3 months ago

{"code":"OK","error":null,"result":{"activation_status":"READY","addresses":{"eth0":["172.20.20.20","172.30.0.11"]},"board_number":"","board_type":"CL","brand":"atrea.cz","cloud":{"enable":false,"link":"https://amotion.cloud","support":true},"commissioned":false,"initialized":true,"localisation":"cs","name":"DUPLEX 380 ECV5.aM-CL","port":80,"production_number":"","service_name":"","type":"DUPLEX 380 ECV5.aM-CL","version":"ATC-v2.2.1"}}

xbezdick commented 3 months ago

Looking into it it feels I should rewrite the plugin to use WS instead. Will give it a look.

xbezdick commented 3 months ago

social@guca:~$ python Python 3.12.3 (main, Apr 17 2024, 00:00:00) [GCC 14.0.1 20240411 (Red Hat 14.0.1-0)] on linux Type "help", "copyright", "credits" or "license" for more information.

import websocket ws = websocket.WebSocket() ws = websocket.WebSocket() KeyboardInterrupt ws.connect("ws://172.30.0.11/api/ws",cookie="ATCtoken",origin="testing_websockets-client.com", host="172.30.0.11") ws.send("Hello, Server") 19 print(ws.recv()) {"code":"PARSE_ERROR","error":"ParseError: [json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'H'","id":null,"response":null,"type":"response"} ws.send("{"enspoint": "ui_diagram_data","args":null}") File "", line 1 ws.send("{"enspoint": "ui_diagram_data","args":null}") ^^^^^^^^^^^ SyntaxError: invalid syntax. Perhaps you forgot a comma? ws.send('{"endpoint": "ui_diagram_data","args":null}') 49 print(ws.recv()) {"code":"OK","error":null,"id":null,"response":{"ui_diagram_data":{"bypass_estim":53,"damper_io_state":true,"fan_eta_operating_time":0,"fan_sup_operating_time":0,"preheater_active":false,"preheater_factor":0,"preheater_type":"ELECTRO_PWM"}},"type":"response"}

alexelite commented 3 months ago

Thank you again. So you have e Legendary board, and mine is Elementary. I was unable to find any reference about the API implementation. The email the documentation you have is no longer active.

Standard aMotion control functions
Elementary aM-CE basic module
– EC fans speed control (according to selected mode)
– Automatic heat and cool recovery control (by-pass control)
– Evaluates and prevents all emergency conditions according to the measured values
– Possibility of setting basic and user scenes and weekly calendars to select modes,
power, temperatures and other functions
– Ethernet connection for communication over the Internet
– Inputs for external signals - control e.g. from kitchens, toilets and similar
– Possibility of connecting air quality sensors (e.g. CO concentration or relative 2
humidity) either by contact, 0–10V voltage, or via the bus.
– Outputs for continuous control of electric preheater and heater (pulse switched 10 V)
– Possibility of connecting up to two controllers of different types
– Connection to supervisory control system via Modbus TCP protocol

Legendary aM-CL advanced module (with all functions from Elementary
aM-CE module and additional options below)
– Control of systems with VAV boxes
– Control of systems with heat sources (heat pumps, heat accumulators etc.)
– Communication by BACnet protocol over the bus
– Possibility of connecting more than two controllers
– More than 4 external bus elements
(controllers, CO sensors, outdoor temperature sensors,….) 2
– Multiple adjustable scenes (more than 10)
– More than 2 user calendars
– More than 4 users (excluding service access)
xbezdick commented 3 months ago

so websockets attempt no1 didn't work out as there is websockets library and websocket and websocket-client ... I'll have another look later today.

Buuut tldr your api will for sure work with websockets - you can test the websocket-client example in python against your board. Tldr I did tcpdump to confirm the trafic on the websocket and it's what is expected eg { endpoint: <endpoint from the /api list>, args: } with cookie from /login api.

alexelite commented 3 months ago

Success. The websocket works. Token is accepted. Control also works. I tested some commands.

{"endpoint":"login","args":{"token":"ATCtoken"},"id":1}
{"endpoint":"user","args":null,"id":2}
{"endpoint":"time","args":null,"id":3}

#{"endpoint":"control","args":{"variables":{"fan_power_req":1}},"id":24}
#{"endpoint":"control","args":{"variables":{"fan_power_req":14}},"id":43}

#{"endpoint":"control_admin/config/activate_scene","args":{"sceneId":4},"id":31}
#{"endpoint":"control_admin/config/get_scenes","args":null,"id":32}

#{"endpoint":"user/storage/scenesOrder","args":null,"id":33}

#{"endpoint":"control","args":{"variables":{"work_regime":"OFF"}},"id":39}
#{"endpoint":"control","args":{"variables":{"work_regime":"AUTO"}},"id":27}
#{"endpoint":"control","args":{"variables":{"work_regime":"VENTILATION"}},"id":41}
#{"endpoint":"control","args":{"variables":{"work_regime":"NIGHT_PRECOOLING"}},"id":35}
#{"endpoint":"control","args":{"variables":{"work_regime":"DISBALANCE"}},"id":37}
xbezdick commented 3 months ago

Exactly, I dumped the conversation from wireshark and reproduced it by hand. If I manage to kick my self to rewrite this we sould have something soon.

xbezdick commented 3 months ago

Ok so whole requests part can be dropped but there is new issue - you keep getting status whether you want it or not - {"args":{"requests":{"fan_power_req":60,"temp_request":20.0,"work_regime":"VENTILATION"},"states":{"active":{}},"unit":{"fan_eta_factor":60,"fan_sup_factor":60,"mode_current":"NORMAL","season_current":"NON_HEATING","temp_eha":23.4,"temp_eta":23.6,"temp_ida":23.6,"temp_oda":16.5,"temp_oda_mean":17.95,"temp_sup":17.8}},"event":"ui_info","type":"event"}

This means there will have to be some global object updating sensors and status and each time you send something you have to save the id and roll the responses until you get your id.

so this: await hass.async_add_executor_job(ws.connect, "ws://%s/api/ws" % entry.data[CONF_HOST], origin=entry.data[CONF_HOST], host=entry.data[CONF_HOST]) await hass.async_add_executor_job(ws.send, '{"endpoint":"login","args":{"username":"%s","password":"%s"},"id":1}' % (entry.data[CONF_USERNAME],entry.data[CONF_PASSWORD])) token = await hass.async_add_executor_job(ws.recv) await hass.async_add_executor_job(ws.send, '{"endpoint":"login","args":{"token":"%s"},"id":1}' % token)

will not work because I got randomly token = {"args":{"active":false,"countdown":0,"finish":{"day":0,"hour":0,"minute":0,"month":0,"year":0},"sceneId":0,"start":{"day":0,"hour":0,"minute":0,"month":0,"year":0}},"event":"disposable_plan","type":"event"}

alexelite commented 3 months ago

From your response and also the communication monitoring I did, I understand this. The client (ha) requests have an id. When the unit responds to the specific request it uses the same id. Randomly, the unit sends one of 2 types of statuses:

{"args":{"active":false,"countdown":0,"finish":{"day":0,"hour":0,"minute":0,"month":0,"year":0},"sceneId":0,"start":"day":0,"hour":0,"minute":0,"month":0,"year":0}},"event":"disposable_plan","type":"event"}
{"args":{"requests":{"fan_power_req":60,"temp_request":20.0,"work_regime":"VENTILATION"},"states":{"active":{}},"unit":{"fan_eta_factor":60,"fan_sup_factor":60,"mode_current":"NORMAL","season_current":"NON_HEATING","temp_eha":23.4,"temp_eta":23.6,"temp_ida":23.6,"temp_oda":16.5,"temp_oda_mean":17.95,"temp_sup":17.8}},"event":"ui_info","type":"event"}

The status message can be received before the actual response. And it should be discarded or interpreted in another way. The web app uses two type of requests as keep alive or something. {"endpoint":"ping","args":null,"id":314} {"endpoint":"time","args":null,"id":315} I'm not so good at programing so I write this so I can structure the data better in my head.

So the logic of the integration should be:

Is this correct?

xbezdick commented 3 months ago

I got a bit further I think.

in init we should create AmotionAtrea object that will have constructor user,pw,host

internally it should have last message_id so we can increment logically plus each function should save message_id it is going to use?

we will have receive function that will store messages into list by id and just update status if it gets the status messages so when called it first checksif the message id response is in the list before looping on receive

login flow I pasted above - you don't need restapi at all ;.; nor you need cookie or anything you just use login api twice(?) once with login and than with token you got.

xbezdick commented 3 months ago

https://github.com/xbezdick/amotionatrea/blob/websocket/custom_components/amotionatrea/__init__.py something like this. well I will move it forward later. It's too nice outside.

xbezdick commented 3 months ago

so websocket branch now worked for me but I think it's not correct as the logic is not async but I use async all over the place.

alexelite commented 3 months ago

You are awesome.

I installed the latest code and it communicates with the unit. The temperatures are updating now.

xbezdick commented 3 months ago

Nope, it looses connection and is pretty ineffective. I'll have to rewrite it to have some sort of coordinator.

alexelite commented 3 months ago

Hello. I managed to modify the integration and use WebSocketApp method for communication. The connection is stable, this is the recommended way for persistent connection via websocket, as I read. At the moment the sensors and climate platforms use the data already received in the communication routine. A callback is used for all incoming messages. It is now a total mess of code, with hundreds of debug messages. I will be on vacation the following week, but after that, if you want, I can clean it up a little and send it to you. You clearly have more experience coding than me.

Thank you again for your help.

xbezdick commented 3 months ago

So I started branch coordinator which seems to be much better rewrite. Overall I still think the model is wrong as at the moment it polls websocket while the atrea unit clearly pushes updates. So I guess yet another rewrite will be needed to switch coordinator to push model. Still feel free to try the coordinator branch as it seems much more stable.

xbezdick commented 3 months ago

I don't see your changes in your fork so I can't have a look but yes I plan to have on_update or something like that callback.

xbezdick commented 2 months ago

@alexelite can you check this change ^^ it should make it work and it should be stable.

I will close this issue with this pullrequest and we will be able to look into better config_flow and actually adding features.