Krazy998 / mqtt-hisensetv

Details to connect to Hisense Smart TV MQTT broker for home automation
MIT License
121 stars 12 forks source link

Identifier rejected on 55U7KQ #14

Open marcandre83 opened 1 year ago

marcandre83 commented 1 year ago

I bought a 55U7KQ that came out this year. It looks like Hisense did some changes to MQTT again, I am getting “Connection refused: Identifier rejected” over MQTT Explorer as well as within the mqtt logs on my raspberry although I am using my phone’s MAC address as the client ID. Also on this newer model I can not use the Remotenow app, only the VIDAA app. Any help would be highy appreciated.

Bildschirmfoto 2023-10-17 um 16 47 42
zamzon commented 11 months ago

Hi

I have a 100U7KQ from 2023, with the VIDAA system. Is there a finish solution done, or is it on going ?

maks23rus commented 11 months ago

These are the series of hashes you need to use: &vidaa#^app 38D65DC30F45109A369A86FCE866A85B$C0:BD:D1:3D:6E:3E his9h*i&s%e!r^v0i1c9 1701415028$3D5AEF

These are the results 38D65DC30F45109A369A86FCE866A85B 44DE1F1BC56E2737276E4D8F96E4AB53 3D5AEFF5F89E96E412ACF430C630DE9F 97A11ED3305C23F70B8A335F9D4C0CBF

How you get the hashes: The 1st line "&vidaa#^app" is fixed. The 2nd line is made up of "38D65DC30F45109A369A86FCE866A85B$" plus the mac address... "38D65DC30F45109A369A86FCE866A85B$" is fixed. The 3rd line is mostly fixed, apart from the digit after "his", in this example it's "his9" This is generated from the unixtime stamp, which in this example was "1701415028", to get 9 you have to add up all the digits 1+7+0+1+4+1+5+0+2+8=29, 9 is the last digit of this sum, 29. (This took me some time to work out as there's no code you can read from the app) The 4th line is the timestamp+$+ the first 6 digits of the 3rd hash "3D5AEF" So from this you now have your login details to connect via MQTT

username his$1701415028 (unix timestamp) client ID :C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001 (client ID 44DE1F is the first six digits of the 2nd hash) password :97A11ED3305C23F70B8A335F9D4C0CBF (4th hash)

God bless you, your family and friends. You are a genius. Thank you very much for your work.

MaxwellJK commented 11 months ago

well done @LeoKlaus that was the final missing bit i assume. Did you try and connect home assistant to your tv by any chance? when i try, even though credentials are correct, it fails. Not entirely sure why 🤔

LeoKlaus commented 11 months ago

well done @LeoKlaus that was the final missing bit i assume. Did you try and connect home assistant to your tv by any chance? when i try, even though credentials are correct, it fails. Not entirely sure why 🤔

Up until now, I haven't used HomeAssistant, but I've spun up the docker container to test connecting to my TV that way. I couldn't manage to connect via MQTT (in HomeAssistant) either. No combination of certificates, credentials and other settings ever let me connect to the TVs broker.

Another issue I had when trying to set up HomeAssistant is that I couldn't find a way to get the MQTT credentials in a dynamic way. I was only able to hardcode them in the integration (which would mean that the integration would stop working every 2 days).

I'm currently trying two different approaches:

  1. Using Mosquitto in bridge mode (couldn't manage to connect, no way to programmatically update password) If someone managed to set that up, you might even be able to use the existing Hisense TV integration for HomeAssistant (sehaas/ha_hisense_tv). From glancing at the code, it does seem that it already uses the client_id in all topics, so it should work once you have a working bridge.
  2. Use Node-RED to connect to the TVs broker (works with hard-coded user name and password, I haven't been able to load them from a file yet) This way I can connect my Harmony Remote directly using the FakeRoku contrib and send translated commands to the TV. Nothing would stop you from using anything else supported by Node-RED, I suppose you could even build an MQTT bridge this way, just connecting the MQTT-IN node to the MQTT-OUT.

The bit I find trickiest really is updating the MQTT credentials in whatever automation software I'm trying to use. Refreshing the token is very easy, I've already modified the Python script to do so:

import paho.mqtt.client as mqtt
import time
import json

client_id =     "C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001"
tv_ip =         "192.168.27.140"
username =      "his$1702984312"
accesstoken =   "_euiPdb79wKiMwM1ouXP+nPoGt9jTNZop5KKBbAevhk9AfvueLGfyGT1n7keQ5Jzw"
refreshtoken =  "#euiPdb79wKiMwM1ouXP+nPoGt9jTNZop5KKBbAevhk87PO0LU4Cj1OSsBGVlBSP/"
certfile =      "./rcm_certchain_pem.cer"
keyfile =       "./rcm_pem_privkey.pkcs8"

credentials = ""

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        client.connected_flag=True #set flag
        print("connected ok")
        # Subscribing in on_connect() means that if we lose the connection and
        # reconnect then subscriptions will be renewed.
        client.subscribe("#")
def on_message(client, userdata, message):
    print("message received " ,str(message.payload.decode("utf-8")))
    global credentials
    credentials = json.loads(message.payload.decode("utf-8"))
def on_publish(client, userdata, mid):
    print("Published message " + str(mid))
def on_disconnect(client, userdata, rc):
    print("disconnecting reason  "  +str(rc))

def refresh_token():

    client = mqtt.Client(client_id=client_id, clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp")

    client.tls_set(ca_certs=None, certfile=certfile, keyfile=keyfile, cert_reqs=mqtt.ssl.CERT_NONE,
        tls_version=mqtt.ssl.PROTOCOL_TLS, ciphers=None)

    client.on_connect = on_connect
    client.on_message = on_message
    # client.on_subscribe = on_subscribe
    client.on_publish = on_publish
    client.on_disconnect = on_disconnect

    client.connected_flag=False

    client.tls_insecure_set(True)

    client.username_pw_set(username=username, password=refreshtoken)

    client.connect_async(tv_ip, 36669, 60)
    client.loop_start()

    while not client.connected_flag: #wait in loop
        print("In wait loop")
        time.sleep(1)

    client.publish("/remoteapp/tv/platform_service/" + client_id + "/data/gettoken", '{"refreshtoken": "' + refreshtoken + '"}')

    client.subscribe("/remoteapp/mobile/" + client_id + "/platform_service/data/tokenissuance")

    while credentials == "":
        print("waiting for refreshed credentials...")
        time.sleep(1)

    with open("credentials.json", "w") as file:
        json.dump(credentials, file, indent= 4)

    print("got new credentials, terminating...")
    client.loop_stop()
    client.disconnect()

    exit()

I've chosen to dump the credentials to a file, but you should be able to use credentials like a normal Python dict (i.e. credentials["accesstoken"] to get the accesstoken).

My idea was running the python script every 24 or so hours via cron, outputting the new accesstoken to a file which in turn is read by the MQTT client of the automation software.

LeoKlaus commented 11 months ago

I think I got it!

I'm using Node-RED with the FakeRoku contrib, so you'll probably not be able to copy/paste this, but it should be fairly easy to adapt to your specific needs:

I've created this Python script to update the Node-RED flow with the new access token obtained from the TV:

import requests
import refresh_credentials

url = 'http://192.168.27.210:1880/flows'

newPass = refresh_credentials.refresh_token()
print(newPass)

myobj = [
    {
        "id": "62c05258efaa3b6c",
        "type": "tab",
        "label": "Flow 1",
        "disabled": False,
        "info": "",
        "env": []
    },
    {
        "id": "d7c9e204ccda2377",
        "type": "inject",
        "z": "62c05258efaa3b6c",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": False,
        "onceDelay": 0.1,
        "topic": "/remoteapp/tv/remote_service/C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001/actions/sendkey",
        "payload": "KEY_POWER",
        "payloadType": "str",
        "x": 210,
        "y": 240,
        "wires": [
            [
                "eaf2c4ae00aad25b"
            ]
        ]
    },
    {
        "id": "dfae181e6862b976",
        "type": "fakeroku-device",
        "z": "62c05258efaa3b6c",
        "confignode": "147f0599fe70a7c1",
        "x": 180,
        "y": 400,
        "wires": [
            [
                "9fc9a356bbbc00df"
            ]
        ]
    },
    {
        "id": "9fc9a356bbbc00df",
        "type": "switch",
        "z": "62c05258efaa3b6c",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "Info",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "Search",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "Back",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "Left",
                "vt": "str"
            }
        ],
        "checkall": "True",
        "repair": False,
        "outputs": 4,
        "x": 350,
        "y": 400,
        "wires": [
            [
                "96df6d7ff7322e7a"
            ],
            [
                "3e654eba77f13833"
            ],
            [
                "42119a0d26c82dda"
            ],
            [
                "5c549419f9209d93"
            ]
        ]
    },
    {
        "id": "96df6d7ff7322e7a",
        "type": "change",
        "z": "62c05258efaa3b6c",
        "name": "On/Off",
        "rules": [
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "/remoteapp/tv/remote_service/C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001/actions/sendkey",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "KEY_POWER",
                "tot": "str"
            },
            {
                "t": "delete",
                "p": "action",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": False,
        "x": 510,
        "y": 360,
        "wires": [
            [
                "eaf2c4ae00aad25b"
            ]
        ]
    },
    {
        "id": "3e654eba77f13833",
        "type": "change",
        "z": "62c05258efaa3b6c",
        "name": "HDMI 1",
        "rules": [
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "/remoteapp/tv/ui_service/C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001/actions/changesource",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{\"sourceid\":\"HDMI1\"}",
                "tot": "json"
            },
            {
                "t": "delete",
                "p": "action",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": False,
        "x": 520,
        "y": 420,
        "wires": [
            [
                "eaf2c4ae00aad25b"
            ]
        ]
    },
    {
        "id": "eaf2c4ae00aad25b",
        "type": "mqtt out",
        "z": "62c05258efaa3b6c",
        "name": "Turn On",
        "topic": "",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "203146b562d79293",
        "x": 780,
        "y": 240,
        "wires": []
    },
    {
        "id": "42119a0d26c82dda",
        "type": "change",
        "z": "62c05258efaa3b6c",
        "name": "HDMI 3",
        "rules": [
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "/remoteapp/tv/ui_service/C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001/actions/changesource",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{\"sourceid\":\"HDMI3\"}",
                "tot": "json"
            },
            {
                "t": "delete",
                "p": "action",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": False,
        "x": 520,
        "y": 480,
        "wires": [
            [
                "eaf2c4ae00aad25b"
            ]
        ]
    },
    {
        "id": "5c549419f9209d93",
        "type": "change",
        "z": "62c05258efaa3b6c",
        "name": "HDMI 4",
        "rules": [
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "/remoteapp/tv/ui_service/C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001/actions/changesource",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{\"sourceid\":\"HDMI4\"}",
                "tot": "json"
            },
            {
                "t": "delete",
                "p": "action",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": False,
        "x": 520,
        "y": 540,
        "wires": [
            [
                "eaf2c4ae00aad25b"
            ]
        ]
    },
    {
        "id": "147f0599fe70a7c1",
        "type": "fakeroku-config",
        "ip": "192.168.27.210",
        "multicast": "239.255.255.250",
        "uuid": "8f085b7318ccd20139b18d8646b77805",
        "port": "8086"
    },
    {
        "id": "203146b562d79293",
        "type": "mqtt-broker",
        "name": "Hisense TV",
        "broker": "192.168.27.140",
        "port": "36669",
        "tls": "618954f6dbec284f",
        "clientid": "C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001",
        "autoConnect": True,
        "usetls": True,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": True,
        "autoUnsubscribe": True,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "False",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closeRetain": "False",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willRetain": "False",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": "",
        "credentials": {
            "user": "his$1702984312",
            "password": newPass
        }
    },
    {
        "id": "618954f6dbec284f",
        "type": "tls-config",
        "name": "Hisense Certs",
        "cert": "",
        "key": "",
        "ca": "",
        "certname": "rcm_certchain_pem.cer",
        "keyname": "rcm_pem_privkey.pkcs8",
        "caname": "",
        "servername": "",
        "verifyservercert": False,
        "alpnprotocol": ""
    }
]

x = requests.post(url, json = myobj)

The script contains the entire flow, I haven't found a way to update just the credentials using the Node-RED API and to be honest, I don't need it as my flow will remain very static. The important bits are newPass = refresh_credentials.refresh_token() which calls this script:

import paho.mqtt.client as mqtt
import time
import json

client_id =     "C2:BE:D1:3D:6E:3E$his$DEC5E8_vidaacommon_001"
tv_ip =         "192.168.27.140"
username =      "his$1703090728"
#refreshtoken =  "#ET9VvBPGv4g3ZEmzw5Cn3GgkYtzMqPfof3BQnbby/tWnQ/pO5qMdUSlnUsVQz1re"
certfile =      "./rcm_certchain_pem.cer"
keyfile =       "./rcm_pem_privkey.pkcs8"

try:
    file = open('credentials.json')
except FileNotFoundError:
    print('No stored credentials found, starting auth with TV...')
else:
    with file:
        oldCreds = json.load(file)

refreshtoken = oldCreds["refreshtoken"]

credentials = ""

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        client.connected_flag=True #set flag
        print("connected ok")
        # Subscribing in on_connect() means that if we lose the connection and
        # reconnect then subscriptions will be renewed.
        client.subscribe("#")
def on_message(client, userdata, message):
    print("message received " ,str(message.payload.decode("utf-8")))
    global credentials
    credentials = json.loads(message.payload.decode("utf-8"))
def on_publish(client, userdata, mid):
    print("Published message " + str(mid))
def on_disconnect(client, userdata, rc):
    print("disconnecting reason  "  +str(rc))

def refresh_token():

    client = mqtt.Client(client_id=client_id, clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp")

    client.tls_set(ca_certs=None, certfile=certfile, keyfile=keyfile, cert_reqs=mqtt.ssl.CERT_NONE,
        tls_version=mqtt.ssl.PROTOCOL_TLS, ciphers=None)

    client.on_connect = on_connect
    client.on_message = on_message
    # client.on_subscribe = on_subscribe
    client.on_publish = on_publish
    client.on_disconnect = on_disconnect

    client.connected_flag=False

    client.tls_insecure_set(True)

    client.username_pw_set(username=username, password=refreshtoken)

    client.connect_async(tv_ip, 36669, 60)
    client.loop_start()

    while not client.connected_flag: #wait in loop
        print("In wait loop")
        time.sleep(1)

    client.subscribe("/remoteapp/mobile/" + client_id + "/platform_service/data/tokenissuance")
    client.publish("/remoteapp/tv/platform_service/" + client_id + "/data/gettoken", '{"refreshtoken": "' + refreshtoken + '"}')

    while credentials == "":
        print("waiting for refreshed credentials...")
        time.sleep(1)

    with open("credentials.json", "w") as file:
        json.dump(credentials, file, indent= 4)

    print("got new credentials, terminating...")
    client.loop_stop()
    client.disconnect()

    return credentials["accesstoken"]
    #exit()

(It's basically the same script I've posted earlier, I just moved the subscribe statement before the the publish statement to prevent it from not receiving the response and added a return statement for the new accesstoken)

and

"credentials": {
            "user": "his$1702984312",
            "password": newPass
        }

which sets the new credentials in Node-RED.

Now all that's left to do is run this every day via cron and see if it works in 2 days (and 30)!

chimpzilla commented 10 months ago

Looks like Hisense might be reading this. They did an update on the 24th that removed the MQTT output to the logcat, luckily I have a copy of the older version on a phone that is set not to update.
Anyone yet know what happens after 30 days to the refresh token? I tried forwarding the date 30 days on my phone using the official app, but it then refuses to connect. I manually changed the time on the TV, but this only changes the time displayed on the TV. All the unixtime stamps sent from the TV are still the current time. Maybe Hisense could answer if they're reading this :D

LeoKlaus commented 10 months ago

Looks like Hisense might be reading this. They did an update on the 24th that removed the MQTT output to the logcat, luckily I have a copy of the older version on a phone that is set not to update.

Wow. Good thing I disabled internet access for my TV. Let's hope they don't change auth mechanism...

If there really is someone from Hisense reading: Just give us access. Whatever you're trying to hide is much more likely to be found when people have to pry their devices open. I'd even pay for some sort of MQTT upgrade or whatever, all I want is to control the TV I bought.

Anyone yet know what happens after 30 days to the refresh token?

Interestingly enough, for my unit, the refreshtoken and refreshtoken_time seem to refresh too when refreshing the accesstoken.

I've containerized the python script I've posted above and it's been running since then (just over two weeks) without issues, I can still control the TV via Node-RED. Here's the output for the last three nights:

In wait loop
connected ok
waiting for refreshed credentials...
Published message 3
message received  {
        "accesstoken":  "_ioli/l3tSl+m1LS938mGeMZsJKidXqNRRyNFo2V1UPmvLPykyR4prZdqRdSgiyaX",
        "accesstoken_time":     "1704164402",
        "accesstoken_duration_day":     2,
        "refreshtoken": "#ioli/l3tSl+m1LS938mGeMZsJKidXqNRRyNFo2V1UPmF0Cyz08+mp7hF4hSG5fkU",
        "refreshtoken_time":    "1704164402",
        "refreshtoken_duration_day":    30
}
got new credentials, terminating...
disconnecting reason  0

In wait loop
connected ok
waiting for refreshed credentials...
Published message 3
message received  {
        "accesstoken":  "_12a2EzCDMDwDFeZEc73TU3865RD78EHrg/FetHzLLXOG81k4mtgNY0/vouJUQTGQ",
        "accesstoken_time":     "1704250802",
        "accesstoken_duration_day":     2,
        "refreshtoken": "#12a2EzCDMDwDFeZEc73TU3865RD78EHrg/FetHzLLXMoZVuRWcvG4Ym9sG8kYyRA",
        "refreshtoken_time":    "1704250802",
        "refreshtoken_duration_day":    30
}
got new credentials, terminating...
disconnecting reason  0

In wait loop
connected ok
waiting for refreshed credentials...
Published message 3
message received  {
        "accesstoken":  "_Hwiv+9xvpYpnor+cGWwkkgdfI7jYsqWrTaBnB0NTB/NqAN4sQpqz3nQ7QoFjpjCr",
        "accesstoken_time":     "1704337201",
        "accesstoken_duration_day":     2,
        "refreshtoken": "#Hwiv+9xvpYpnor+cGWwkkgdfI7jYsqWrTaBnB0NTB/NgI6/bWKNbLBZEyJm9C8qI",
        "refreshtoken_time":    "1704337201",
        "refreshtoken_duration_day":    30
}
got new credentials, terminating...
disconnecting reason  0

I'll let you know if anything breaks.

zamzon commented 10 months ago

Do you have a guide to install a the new version in HA.?

LeoKlaus commented 10 months ago

Do you have a guide to install a the new version in HA.?

There is no new version. I've hacked something together using the python scripts above.

You can try adapting those scripts to your own needs or build an MQTT bridge with Node-RED on the basis of the last script I posted and combine that with the sehaas/ha_hisense_tv integration, but I have no intention of ever building a HomeAssistant integration for this, as I don't use HA.

Edit: Hisense made this stuff a lot more complicated for no apparent reason, they're to blame for this shitshow and the lack of controllability via universal-ish home automation standards. If you have an iPhone, you can use HomeKit to control the TV, but other than that, you're basically limited to Hisenses apps.

chimpzilla commented 10 months ago

Interestingly enough, for my unit, the refreshtoken and refreshtoken_time seem to refresh too when refreshing the accesstoken.

Ah, on mine only the access token seems to change. However, I've not tried it for more than a day, just within a minute or two. I guess if you let the 30 days elapse without connecting you have to maybe re-pair.

chimpzilla commented 10 months ago

Interestingly enough, for my unit, the refreshtoken and refreshtoken_time seem to refresh too when refreshing the accesstoken.

When you send the "gettoken{"refreshtoken":......" are you connected using the current access token as the password or with the hashed value password used in the pairing process?

LeoKlaus commented 10 months ago

When you send the "gettoken{"refreshtoken":......" are you connected using the current access token as the password or with the hashed value password used in the pairing process?

You have to connect using the refreshtoken as password.

chimpzilla commented 10 months ago

You have to connect using the refreshtoken as password.

oh, I see, I think.... So you have to connect with the accesstoken to control the TV, but with the refreshtoken to get both tokens refreshed?

LeoKlaus commented 10 months ago

So you have to connect with the accesstoken to control the TV, but with the refreshtoken to get both tokens refreshed?

Exactly. This is also the only time the refreshtoken is actually used, as far as I can tell.

Edit: I didn't try much, but I suspect you can only publish to the gettoken topic using the refreshtoken

chimpzilla commented 10 months ago

Edit: I didn't try much, but I suspect you can only publish to the gettoken topic using the refreshtoken

You can use gettoken topic (you need to subscribe too) when connected to the TV using the access token and it refreshes the access token. However, I'm not sure if you can reconnect using the access token after two days, so maybe then you have to use the refresh token. I've just tried using the refresh token as the password and still all that refreshes is the accesstoken. The refreshtoken that I get back is the same as the token I use in the "gettoken{"refreshtoken":......" in the first place.

What an overcomplicated system, for no apparent reason!

LeoKlaus commented 10 months ago

I've just tried using the refresh token as the password and still all that refreshes is the accesstoken. The refreshtoken that I get back is the same as the token I use in the "gettoken{"refreshtoken":......"

How old is your accesstoken? I mean this could be model dependent (I have a 55U8KQ), but I highly doubt it. My script is running every 24h, so the accesstoken is always 24 hours old on refresh.

chimpzilla commented 10 months ago

I've just tried using the refresh token as the password and still all that refreshes is the accesstoken. The refreshtoken that I get back is the same as the token I use in the "gettoken{"refreshtoken":......"

How old is your accesstoken? I mean this could be model dependent (I have a 55U8KQ), but I highly doubt it. My script is running every 24h, so the accesstoken is always 24 hours old on refresh.

Minutes old as I've been trying to perfect the whole pairing process in my universal remote Android app. I'm not using an MQTT library, just raw TCP commands (MQTT headers are easy to understand and tiny), so it's all a bit more complicated... although more bullet-proof in the long run, as I don't have to deal with the headache of library updates. It's also a lot less code, so a smaller app. I avoid libraries (wherever possible) like the plague, as I like to know exactly what is going on. I guess I'll have to leave it a couple of days and see what happens.

chimpzilla commented 10 months ago

The refresh token does indeed refresh itself after a day or two. This is even when connected with the access token. I'm yet to see if you can still connect with the access token after 2 days of not connecting or if you need to connect with the refresh token.

LeoKlaus commented 10 months ago

The refresh token does indeed refresh itself after a day or two. This is even when connected with the access token.

Interesting. Didn't expect that...

chimpzilla commented 10 months ago

I've found a bit more out. If an accessToken is more than 2 days old then you must connect with the refreshToken to get a new accessToken (and new refresh token). You then have to reconnect with the new accessToken. I've set a rule in my code that if the refreshToken is older than 30 days then pairing must begin again, as I suspect (but can't wait 30 days to find out) that you cannot connect with the refresh token if it's older than 30 days. There is no real reason for any of this, security via spaghetti!

LeoKlaus commented 10 months ago

security via spaghetti

I'll start using this, haha.

nikagersonlohman commented 10 months ago

Thanks for this! I cannot get it to work yet with my 65U7KQ.

I've done the following:

  1. Downloaded and extracted hi_keys.zip from the mentioned repo
  2. Extracted the access (with an _) and refresh (with a #) tokens from the deviceconnectbean table in comm.db from my Android device (with working Vidaa app)
  3. Extracted the ip-address from the devicebean table (= same as ip mentioned on TV :-) )
  4. Created a credentials.json file: { "refreshtoken": "TOKEN_HERE" }

It remains in the waiting loop with disconnect_reason 5.

I have tried the refresh and access tokens in the credentials.json file, neither seems to get it out of the loop.

I did not modify the client_id. Should I get that from somewhere?

What am I missing?

Regards,

Nika.

LeoKlaus commented 10 months ago

@nikagersonlohman

You'll have to use either one of the scripts posted earlier or use the guide @chimpzilla posted.

You will need the following from a matching set:

and the hi_keys, to connect.

Client id, username and password depend on the MAC address of the device you used (though they aren't verified), so you won't be able to use the tokens from your android device without knowing its client id.

nikagersonlohman commented 10 months ago

Cool, thanks!! So here's a python script for that :)

import getmac
import hashlib
import datetime
import time

mac_address = getmac.get_mac_address()
unix_time = str(int(time.time()))

unix_time_sum = sum(int(digit) for digit in unix_time)
unix_time_sum_lastdigit = str(unix_time_sum % 10)

decodedline1 = "&vidaa#^app"
hashline1 = hashlib.md5(decodedline1.encode()).hexdigest().upper()

decodedline2 = hashline1 + "$" + mac_address
hashline2 = hashlib.md5(decodedline2.encode()).hexdigest().upper() 

decodedline3 = "his" + unix_time_sum_lastdigit + "h*i&s%e!r^v0i1c9"
hashline3 = hashlib.md5(decodedline3.encode()).hexdigest().upper()

decodedline4 = unix_time + "$" + hashline3[:6]
hashline4 = hashlib.md5(decodedline4.encode()).hexdigest().upper()

print(decodedline1)
print(decodedline2)
print(decodedline3)
print(decodedline4)

print(hashline1)
print(hashline2)
print(hashline3)
print(hashline4)

username = "his$" + unix_time
client_id = mac_address + "$his$" + hashline2[:6] + "_vidaacommon_001"
password = hashline4

print(username)
print(client_id)
print(password)
nikagersonlohman commented 10 months ago

I am planning to do the whole thing in python so it can be reused for other home automation and such. Initial authentication works with the PIN and such, I am stuck though getting the tokens... going to let it rest for a while, have been trying for hours but can't get it to work. So.... still a "big"work in progress:

import paho.mqtt.client as mqtt
import json
import getmac
import hashlib
import time
import datetime
import sys

tv_ip =         "192.168.178.152"
certfile =      "./rcm_certchain_pem.cer"
keyfile =       "./rcm_pem_privkey.pkcs8"
credfile =      "./credentials.json"
mac_address =   getmac.get_mac_address().upper()
unix_time =     str(int(time.time()))

authjson = {
    "client_id": None,
    "username": None,
    "refreshtoken": None,
    "refreshtoken_time": None,
    "refreshtoken_duration_day": None,
    "accesstoken": None,
    "accesstoken_time": None,
    "accesstoken_duration_day": None
}

global mqtt_message
mqtt_message = "undefined"

def load_authjson(file):
    loaded_json = json.load(file)

    authjson["client_id"]                 = loaded_json["client_id"]
    authjson["username"]                  = loaded_json["username"]
    authjson["refreshtoken"]              = loaded_json["refreshtoken"]
    authjson["refreshtoken_time"]         = loaded_json["refreshtoken_time"]
    authjson["refreshtoken_duration_day"] = loaded_json["refreshtoken_duration_day"]
    authjson["accesstoken"]               = loaded_json["accesstoken"]
    authjson["accesstoken_time"]          = loaded_json["accesstoken_time"]
    authjson["accesstoken_duration_day"]  = loaded_json["accesstoken_duration_day"]

def initial_auth():
    unix_time_sum = sum(int(digit) for digit in unix_time)
    unix_time_sum_lastdigit = str(unix_time_sum % 10)

    print("Connecting to Hisense TV...")

    print("")
    decodedline1 = "&vidaa#^app"
    print("Decoded line 1: " + decodedline1)
    hashline1 = hashlib.md5(decodedline1.encode()).hexdigest().upper()
    print("Hashed line 1:  " + hashline1)

    print("")
    decodedline2 = hashline1 + "$" + mac_address
    print("Decoded line 2: " + decodedline2)
    hashline2 = hashlib.md5(decodedline2.encode()).hexdigest().upper() 
    print("Hashed line 2:  " + hashline2)

    print("")
    decodedline3 = "his" + unix_time_sum_lastdigit + "h*i&s%e!r^v0i1c9"
    print("Decoded line 3: " + decodedline3)
    hashline3 = hashlib.md5(decodedline3.encode()).hexdigest().upper()
    print("Hashed line 3:  " + hashline3)

    print("")
    decodedline4 = unix_time + "$" + hashline3[:6]
    print("Decoded line 4: " + decodedline4)
    hashline4 = hashlib.md5(decodedline4.encode()).hexdigest().upper()
    print("Hashed line 4:  " + hashline4)

    authjson["client_id"] = mac_address + "$his$" + hashline2[:6] + "_vidaacommon_001"
    authjson["username"] = "his$" + unix_time
    authjson["refreshtoken"] = hashline4

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        client.connected_flag=True #set flag
        print("")
        print("Connected!")
        client.subscribe("#")

def on_message(client, userdata, message):
    print("")
    print("Message received: ", str(message.payload.decode("utf-8")))
    global mqtt_message
    mqtt_message = json.loads(message.payload.decode("utf-8"))

def on_publish(client, userdata, mid):
    print("")
    print("Published message: " + str(mid))

def on_disconnect(client, userdata, rc):
    print("")
    print("Disconnecting reason:   " + str(rc))
    print("Disconnecting client:   ", client)
    print("Disconnecting userdata: ", userdata)

def get_refresh_token():
    global mqtt_message
    print("")
    print("Connecting to:  " + tv_ip)
    print("With client_id: " + authjson["client_id"])
    print("Username:       " + authjson["username"])
    print("Refresh token:  " + authjson["refreshtoken"])

    client = mqtt.Client(client_id=authjson["client_id"], clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp")
    client.tls_set(ca_certs=None, certfile=certfile, keyfile=keyfile, cert_reqs=mqtt.ssl.CERT_NONE, tls_version=mqtt.ssl.PROTOCOL_TLS, ciphers=None)
    client.on_connect = on_connect
    client.on_message = on_message
    # client.on_subscribe = on_subscribe
    client.on_publish = on_publish
    client.on_disconnect = on_disconnect
    client.connected_flag=False
    client.tls_insecure_set(True)
    client.username_pw_set(username=authjson["username"], password=authjson["refreshtoken"])
    client.connect_async(tv_ip, 36669, 60)
    client.loop_start()

    print("")
    while not client.connected_flag: #wait in loop
        print("In wait loop...")
        time.sleep(1)

    # subscribe: /remoteapp/mobile/C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001/ui_service/data/authentication
        # receivedmsg:
    subscription_topic = "/remoteapp/mobile/" + authjson["client_id"] + "/ui_service/data/authentication"
    print("")    
    print("Subscription topic:   " + subscription_topic)
    client.subscribe(subscription_topic)

    # subscribe: /remoteapp/mobile/C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001/ui_service/data/authenticationcode
        # receivedmsg: {"result":1,"info":""}
        # or if incorrect pin
        # receivedmsg: {"result":100,"info":"Wrong authNum!!"} 
    subscription_topic = "/remoteapp/mobile/" + authjson["client_id"] + "/ui_service/data/authenticationcode"
    print("")    
    print("Subscription topic:   " + subscription_topic)
    client.subscribe(subscription_topic)

    # publish topic: /remoteapp/tv/ui_service/C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001/actions/vidaa_app_connect
        # message: {"app_version":2,"connect_result":0,"device_type":"Mobile App"}
        # Pin should show on screen.
    publish_topic = "/remoteapp/tv/ui_service/" + authjson["client_id"] + "/actions/vidaa_app_connect"
    publish_message = '{"app_version": 2, "connect_result": 0, "device_type": "Mobile App"}'
    print("")    
    print("Publishing topic:   " + publish_topic)
    print("Publishing message: " + publish_message)
    client.publish(publish_topic, publish_message)

    print("")
    while mqtt_message == "undefined":
        print("Waiting for refreshed message...")
        time.sleep(1)

    if mqtt_message == "":
        mqtt_message = { "result": 0}

    if mqtt_message["result"] != 1:
        mqtt_message = "undefined"
        pin_code = input("Please enter the PIN: ")

        # publish topic :/remoteapp/tv/ui_service/C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001/actions/authenticationcode
            # message: {"authNum":"5299"}
            # 5299 is the pin that is shown on the TV, when this is sent the pin should vanish from the screen.
        publish_topic = "/remoteapp/tv/ui_service/" + authjson["client_id"] + "/actions/authenticationcode"
        publish_message = '{"authNum":"' + pin_code + '"}'
        print("")
        print("Publishing topic:   " + publish_topic)
        print("Publishing message: " + publish_message)
        client.publish(publish_topic, publish_message)
    else:
        print("Already authenticated with PIN!")

    print("")
    while mqtt_message == "undefined":
        print("Waiting for refreshed message...")
        time.sleep(1)

    if mqtt_message == "":
        mqtt_message = { "result": 0}

    if mqtt_message["result"] != 1:
        print("Incorrect result received, please fix!")
        sys.exit()

    mqtt_message = "undefined"
    # publish topic:/remoteapp/tv/platform_service/C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001/data/gettoken
        # message:{"refreshtoken":""}
    # client.publish("/remoteapp/tv/platform_service/" + client_id + "/data/gettoken", '{"refreshtoken": "' + password + '"}')

# Publishing topic:   /remoteapp/tv/platform_service/98:59:7A:5B:E6:09$his$0861D2_vidaacommon_001/data/gettoken
# Publishing message: {"refreshtoken":""}

    # subscribe: /remoteapp/mobile/C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001/platform_service/data/tokenissuance
        # receivedmsg:{
        # "accesstoken": "_ZN5aYxg0DNMirIAXJ5OxWZ8mJaUcdR4GzkHXBofpx0a84hQAcbIaHhMxMhO/4hLf",
        # "accesstoken_time": "1702028255",
        # "accesstoken_duration_day": 2,
        # "refreshtoken": "#ZN5aYxg0DNMirIAXJ5OxWZ8mJaUcdR4GzkHXBofpx0Z5nuorpAkQ9hUjZT8JfRRp",
        # "refreshtoken_time": "1702028255",
        # "refreshtoken_duration_day": 30
        # }
    # client.subscribe("/remoteapp/mobile/" + client_id + "/platform_service/data/tokenissuance")

# Subscription topic:   /remoteapp/mobile/98:59:7A:5B:E6:09$his$0861D2_vidaacommon_001/platform_service/data/tokenissuance

    subscription_topic = "/remoteapp/mobile/" + authjson["client_id"] + "/platform_service/data/tokenissuance"
    print("")
    print("Subscription topic:   " + subscription_topic)
    client.subscribe(subscription_topic)

    publish_topic = "/remoteapp/tv/platform_service/" + authjson["client_id"] + "/data/gettoken"
    publish_message = '{"refreshtoken": "' + authjson["refreshtoken"] + '"}'
    print("")
    print("Publishing topic:   " + publish_topic)
    print("Publishing message: " + publish_message)
    client.publish(publish_topic, publish_message)

    # print("")
    # while mqtt_message == "undefined":
    #     print("Waiting for refreshed message...")
    #     time.sleep(1)

    # if mqtt_message == "":
    #     mqtt_message = { "result": 0}

    # if mqtt_message["result"] != 1:
    #     print("Incorrect result received, please fix!")
    #     sys.exit()

    # # publish topic:/remoteapp/tv/ui_service/C0:BD:D1:3D:6E:3E$his$44DE1F_vidaacommon_001/actions/authenticationcodeclose
    #     # message:
    # publish_topic = "/remoteapp/tv/ui_service/" + authjson["client_id"] + "/actions/authenticationcodeclose"
    # publish_message = ''
    # print("")
    # print("Publishing topic:   " + publish_topic)
    # print("Publishing message: " + publish_message)
    # client.publish(publish_topic, publish_message)

    print("")
    while mqtt_message == "undefined":
        print("Waiting for refreshed message...")
        time.sleep(1)

    if mqtt_message == "":
        mqtt_message = { "refreshtoken": "" }

    if mqtt_message["refreshtoken"] != "":
        print("Refreshtoken received: " + mqtt_message["refreshtoken"])
        authjson["refreshtoken"] = mqtt_message["refreshtoken"]

    # with open(credfile, "w") as file:
    #     json.dump(credentials, file, indent= 4)

    #print("Got new credentials" + mqtt_message + ", terminating...")
    client.loop_stop()
    client.disconnect()

def days_difference(timestamp1, timestamp2):
    # Convert Unix timestamps to datetime objects
    date1 = datetime.utcfromtimestamp(timestamp1)
    date2 = datetime.utcfromtimestamp(timestamp2)

    # Calculate the difference in seconds
    time_difference = abs(date2 - date1).total_seconds()

    # Calculate the difference in days
    days_difference = time_difference / (24 * 3600)

    return days_difference

try:
    file = open(credfile)
except FileNotFoundError:
    print("")
    print('No stored credentials found, starting auth with TV...')
    initial_auth()
else:
    with file:
        load_authjson(file)
        print("Current time:     " + unix_time)
        print("Refresh token:   ", authjson["refreshtoken"])
        print("Refresh time:    ", authjson["refreshtoken_time"])
        print("Refresh duration:", authjson["refreshtoken_duration_day"])

        if authjson["refreshtoken"] == "":
            print('No stored credentials found, starting auth with TV...')
            initial_auth()

        if authjson["refreshtoken"] and authjson["refreshtoken_time"] and authjson["refreshtoken_duration_day"]:
            print('Stored credentials found, checking expiration...')
            if days_difference(unix_time, authjson["refreshtoken_time"]) < authjson["refreshtoken_duration_day"]:
                print('Stored credentials expired, starting auth with TV...')
                initial_auth()

get_refresh_token()

with open(credfile, "w") as file:
    json.dump(authjson, file, indent= 4)
LeoKlaus commented 10 months ago

@nikagersonlohman We've already managed to produce a working Python script to both obtain the token and refresh it:

Obtaining new token

Refreshing token (second part)

Keep in mind that the TV does have some sort of flooding protection/blacklist, so you might have to use a different MAC address if you've experimented a lot with one device/MAC. This isn't really a problem though as the TV doesn't verify the MAC address.

If you want to spend some more time on this, I'm sure it could be streamlined a lot. I've ended up using a combination of these scripts with Node-RED to control my TV.

I think the best approach for building a HASS integration would be working with sehaas/ha_hisense_tv and building a custom MQTT bridge that handles the token authentication on its own. It should be fairly easy to do that using the Node-RED template I provided, just replacing the Fake-Roku with an MQTT in, but I don't use HASS, so I didn't try that.

nikagersonlohman commented 10 months ago

Wow, this is great again. For some reason it didn't show that entire post with the Obtaining a new token script! All that time wasted trying to create what you guys had created already! We should publish it on a github account to make collaboration easier... I can do that if you guys want?

nikagersonlohman commented 10 months ago

Note that I have OpenHAB and not HASS and will therefore convert it to javascript (ecmascript) as soon as it works...

nikagersonlohman commented 10 months ago

I think all that was missing in my script was "hotelmodechange"...

LeoKlaus commented 10 months ago

We should publish it on a github account to make collaboration easier... I can do that if you guys want?

Sure, just keep in mind that some of of this may be considered intellectual property of Hisense (especially the certificates).

I can't say whether I'll take the time to contribute to this, as I've got everything I wanted working, but I'd surely appreciate the effort.

zamzon commented 10 months ago

We should publish it on a github account to make collaboration easier... I can do that if you guys want?

Sure, just keep in mind that some of of this may be considered intellectual property of Hisense (especially the certificates).

I can't say whether I'll take the time to contribute to this, as I've got everything I wanted working, but I'd surely appreciate the effort.

Do you have it perfectly up and running now?

What platform are you using, I use HA and NodeRED, can run on this?

LeoKlaus commented 10 months ago

Do you have it perfectly up and running now?

What platform are you using, I use HA and NodeRED, can run on this?

I'm using Node-RED with the FakeRoku adapter to be able to set inputs with my Harmony remote, nothing more. This repo contains some more commands to control the TV, they seem to still work with newer TVs.

You can use the scripts I posted earlier to connect via Node-RED. From there on, you'll have to build your own integration for whatever you need.

I guess with HASS, it should be fairly easy to send webhooks to trigger actions through Node-RED, but I'm not familiar with either, so I can't help you there.

leeandy1 commented 9 months ago

I'm in the same boat after (foolishly) letting my 43A6BGTUK update its firmware.

chimpzilla commented 9 months ago

I'm in the same boat after (foolishly) letting my 43A6BGTUK update its firmware.

That's a 2021 model I think. I wonder if all models are getting this "update"? If you go to http://:38400/MediaServer/rendererdevicedesc.xml what does the "transport_protocol=" value show? I've been trying to get my hands on an older model to test with, but if they are all getting the "update" then I don't really need to bother.

leeandy1 commented 9 months ago

Transport protocol is 3160

I contacted Hisense and they sent me a downgrade firmware. I applied it but still no joy in getting access back.

It looks like the downgrade firmware is still running VIDAA 7 as the RemoteNOW app still does not work.

leeandy1 commented 9 months ago

Just to add some more context to this. I am not sure what the firmware was before the OTA update. The OTA update was V0007.06.30F.N1027 I contacted Hisense UK and told them MQTT is now locked down. They sent me V0007.06.30O.N0829. The RemoteNOW still does not work just the VIDAA app. I cannot get MQTT Explorer to connect to the TV.

I have contacted Hisense again to see what they suggest. I also contacted VIDAA who came back with the following:

"Thank you for contacting Vidaa Customer Support. We're so sorry for the late response, Regrettably, it is not publicly available for anyone who wants to integrate with our TV but for the partners we sign with to enhance our platform. We sincerely apologize for any inconvenience that this might cause you. If you are interested in becoming a partner with Vidaa you can contact our team at https://www.vidaa.com/partners/ "

Sounds like this will be a no go unless Hisense can provide the right downgrade software.

chimpzilla commented 9 months ago

On the TVs I've looked at, >3000 is the new authentication and under 3000 is the older way

leeandy1 commented 9 months ago

Hisense sent another firmware file. I'm now running V0007.06.12R.N0508. MQTT Explorer works as does the RemoteNOW app. I am now having trouble getting the https://github.com/sehaas/ha_hisense_tv integration to work. I just get the spinning wheel and the tv does not bring up a code.

This is so frustrating.

zamzon commented 9 months ago

Hisense sent another firmware file. I'm now running V0007.06.12R.N0508. MQTT Explorer works as does the RemoteNOW app. I am now having trouble getting the https://github.com/sehaas/ha_hisense_tv integration to work. I just get the spinning wheel and the tv does not bring up a code.

This is so frustrating.

Could you share the firmware file?

leeandy1 commented 9 months ago

The link will be valid for 7 days:

https://we.tl/t-gvp3a03abx

chimpzilla commented 9 months ago

Has anybody had any luck getting the picture setting info or channel list from the new firmware? With the older sets you could publish /remoteapp/tv/platform_service/clientID/actions/channellist {"list_para":"1#0","list_name":"Antenna"}

and you would get a list of the current TV channels the TV has.

Also older versions would have the following: Subscribe /remoteapp/mobile/broadcast/platform_service/data/picturesetting

Publish /remoteapp/tv/platform_service/clientID/actions/picturesetting action: { "action": "get_menu_info" }

But I can't get this to work or figure out what the subscribe would be for it to work?

jamesmule commented 7 months ago

EDIT: RESOLVED

Hey there, I encountered this thread two weeks ago, and managed to control the TV through Home Assistant sensors, switches and number entities using some rudimentary manual YAML configuration. But, as of a few days ago the sendkey topic seems to have stopped working, can anyone confirm that?

The topic is this one (excluding my client id): /remoteapp/tv/remote_service/00:00:00:00:00:00$his$000000_vidaacommon_001/actions/sendkey

Tested with payloads KEY_POWER and KEY_MUTE among others.

Note that every other topic in the spreadsheet posted by LeoKlaus still works, the sendkey topic used to work until a few days ago and that I'm using two different clients (with different client ids) to control the TV (which also worked fine).

LeoKlaus commented 7 months ago

Hey there, I encountered this thread two weeks ago, and managed to control the TV through Home Assistant sensors, switches and number entities using some rudimentary manual YAML configuration. But, as of a few days ago the sendkey topic seems to have stopped working, can anyone confirm that?

The topic is this one (excluding my client id): /remoteapp/tv/remote_service/00:00:00:00:00:00$his$000000_vidaacommon_001/actions/sendkey

Tested with payloads KEY_POWER and KEY_MUTE among others.

Note that every other topic in the spreadsheet posted by LeoKlaus still works, the sendkey topic used to work until a few days ago and that I'm using two different clients (with different client ids) to control the TV (which also worked fine).

Did your TV perform a firmware update by chance? Maybe they changed something.

jamesmule commented 7 months ago

It's working again, but I can only guess what the problem was. Maybe I was just stupid somewhere.

One of my two access tokens expired a few hours ago, that might be related. Interestingly though, all the other topics kept working on both clients and the sendkey topic stopped working on both clients.

stevepbuk commented 6 months ago

Hi, Ive tried the above python routine to generate a access token and at first I was joyed as it connected from MQTT Explorer. However, if I publish events they dont seem to action on the TV. I have also tried the same configuration on Home Assistant, and it seems to like the configuration ( doesnt error ) but again it does not appear to be receiving any events or allowing me to publish simple events to the TV. Ive also tried in MQTTX which connects fine but gives the following error when I try to subscribe to anything "Failed to Subscribe TOPIC, Error: Not authorized(Code: 135). Make sure the permissions are correct, and check MQTT broker ACL configuration". I am running a brand new 2023 model - Hisense 55U8KQTUK. However, the scripts that were written on this thread appear to work fine and they are publishing and subscribing with no problem.

Thanks

Steve

catt0 commented 6 months ago

I have a Hisense 65E7KQ PRO, bought via Amazon DE in Dec 2023 with software V0002.07.50B.N0715. Thanks to this thread I can connect to it via MQTT. I slightly modified the two scripts provided, call refresh_token.py and it will either create a new set of credentials or refresh an existing one. The credentials will be saved in credentials.json. You will be asked for the PIN code shown on the TV if a fresh set is created. I confirmed I can control and view the TV state via MQTT Explorer.

Some notes, in case they help someone:

Next steps (in the medium term) for me are to pretty up this code and try my hand at creating my first HASS integration or modifying an existing one. Not sure how to handle the client certs yet. Sadly you can't easily extract them from the APK.

generate_token.py:

import re, uuid
import hashlib
import time
import paho.mqtt.client as mqtt
import json
from pprint import pprint

reply = None

authentication_payload = None
authentication_code_payload = None
tokenissuance = None

topicTVUIBasepath = None
topicTVPlatformBasepath = None
topicMobileBasepath = None

def cross_sum(n):
   r = 0
   while n:
       r, n = r + n % 10, n // 10
   return r

def stringToHash(input: str):
    result = hashlib.md5(input.encode("utf-8"))
    return result.hexdigest().upper()

def on_connect(client, userdata, flags, rc):
    global topicTVUIBasepath
    global topicTVPlatformBasepath
    global topicMobileBasepath
    if rc == 0:
        client.connected_flag=True #set flag
        print("connected ok")

        # Subscribing in on_connect() means that if we lose the connection and
        # reconnect then subscriptions will be renewed.
        client.subscribe("#")

        client.subscribe(topicTVUIBasepath + "actions/vidaa_app_connect")
        client.subscribe(topicMobileBasepath + 'ui_service/data/authentication')
        client.subscribe(topicMobileBasepath + 'ui_service/data/authenticationcode')

        client.subscribe("/remoteapp/mobile/broadcast/ui_service/data/hotelmodechange")

        client.subscribe(topicMobileBasepath + 'platform_service/data/tokenissuance')

    else:
        print("Bad connection Returned code=",rc)
        client.bad_connection_flag=True

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, message):
    print("i'm on message")
    global reply
    print("message received " ,str(message.payload.decode("utf-8")))
    print("message topic=",message.topic)
    print("message qos=",message.qos)
    print("message retain flag=",message.retain)
    reply = message

def on_subscribe(client, userdata, mid, granted_qos):
    print("Subscribed: "+str(mid)+" "+str(granted_qos))

def on_publish(client, userdata, mid):
    print("Published message " + str(mid))

def on_disconnect(client, userdata, rc):
    print("disconnecting reason  "  +str(rc))

def on_log(client, userdata, level, buf):
    print("log: ",buf)

def on_message_msgs(mosq, obj, msg):
    print("MESSAGES: " + msg.topic + " " + str(msg.qos) + " " + str(msg.payload))

def on_authentication(mosq, obj, msg):
    global authentication_payload
    authentication_payload = msg

def on_authentication_code(mosq, obj, msg):
    global authentication_code_payload
    authentication_code_payload = msg

def on_tokenissuance(mosq, obj, msg):
    global tokenissuance
    tokenissuance = msg

def write_token_to_creds_file():
    global topicTVUIBasepath
    global topicTVPlatformBasepath
    global topicMobileBasepath
    timestamp = int(time.time())
    #timestamp = 1702583685

    firstHash = stringToHash("&vidaa#^app")

    mac = ':'.join(re.findall('..', '%012x' % uuid.getnode())).upper()
    #mac = "C1:BD:D1:3D:6E:3E"
    print(f'mac {mac}')

    secondHash = stringToHash("38D65DC30F45109A369A86FCE866A85B$" + mac)

    lastDigitOfCrossSum = cross_sum(timestamp)%10

    thirdHash = stringToHash("his"+ str(lastDigitOfCrossSum) +"h*i&s%e!r^v0i1c9")

    fourthHash = stringToHash(str(timestamp) + "$" + thirdHash[:6])

    print(firstHash)
    print(secondHash)
    print(thirdHash)
    print(fourthHash)

    clientID = mac + "$his$" + secondHash[:6] + "_vidaacommon_001"

    client = mqtt.Client(client_id=clientID, clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp")

    client.tls_set(ca_certs=None, certfile="./rcm_certchain_pem.cer", keyfile="./rcm_pem_privkey.pkcs8", cert_reqs=mqtt.ssl.CERT_NONE,
        tls_version=mqtt.ssl.PROTOCOL_TLS, ciphers=None)

    client.on_connect = on_connect
    client.on_message = on_message
    # client.on_subscribe = on_subscribe
    client.on_publish = on_publish
    client.on_disconnect = on_disconnect
    # client.on_log = on_log

    client.connected_flag=False

    client.tls_insecure_set(True)

    username = "his$" + str(timestamp)
    print(f'username: {username}')
    print(f'password: {fourthHash}')
    print(f'client_id: {clientID}')

    client.username_pw_set(username=username, password=fourthHash)

    topicTVUIBasepath = "/remoteapp/tv/ui_service/" + clientID + "/"
    topicTVPlatformBasepath = "/remoteapp/tv/platform_service/" + clientID + "/"
    topicMobileBasepath = "/remoteapp/mobile/" + clientID + "/"

    client.message_callback_add(topicMobileBasepath + 'ui_service/data/authentication'          , on_authentication)
    client.message_callback_add(topicMobileBasepath + 'ui_service/data/authenticationcode'      , on_authentication_code)

    client.message_callback_add('/remoteapp/mobile/broadcast/ui_service/data/hotelmodechange'   , on_message_msgs)

    client.message_callback_add(topicMobileBasepath + 'platform_service/data/tokenissuance'     , on_tokenissuance)

    client.connect_async("192.168.65.61", 36669, 60)
    client.loop_start()

    while not client.connected_flag: #wait in loop
        print("In wait loop")
        time.sleep(1)

    print('publishing message to actions/vidaa_app_connect ...')
    client.publish( topicTVUIBasepath + "actions/vidaa_app_connect", '{"app_version":2,"connect_result":0,"device_type":"Mobile App"}')

    print(f'subscribing to {topicMobileBasepath}ui_service/data/authentication ...')
    while authentication_payload is None:
        time.sleep(0.1)

    if authentication_payload.payload.decode() != '""' :
        print('Problems with the authentication message!')
        print(authentication_payload.payload)
        print('Exiting...')
        exit()

    authNum = input("Enter the four digits displayed on your TV: ")

    print(f'publishing message to {topicTVUIBasepath}actions/authenticationcode ...')
    client.publish( topicTVUIBasepath + "actions/authenticationcode", '{"authNum":' + authNum + '}')

    print(f'subscribing to {topicMobileBasepath}ui_service/data/authenticationcode ...')

    client.subscribe(topicMobileBasepath + 'ui_service/data/authenticationcode')

    while authentication_code_payload is None:
        time.sleep(0.1)

    print(authentication_code_payload.payload.decode())
    if json.loads(authentication_code_payload.payload.decode()) != json.loads('{"result": 1,"info": ""}') :
        print('Problems with the authentication message!')
        print(authentication_code_payload.payload)
        print('Exiting...')
        exit()

    print("Success! Getting access token...")
    print(f'publishing message to {topicTVPlatformBasepath}data/gettoken ...')
    client.publish( topicTVPlatformBasepath + "data/gettoken", '{"refreshtoken": ""}')

    print(f'publishing message to {topicTVUIBasepath}actions/authenticationcodeclose ...')
    client.publish( topicTVUIBasepath + "actions/authenticationcodeclose")

    print(f'subscribing to /remoteapp/mobile/broadcast/ui_service/data/hotelmodechange ...')
    client.subscribe('/remoteapp/mobile/broadcast/ui_service/data/hotelmodechange')

    print(f'subscribing to {topicMobileBasepath}platform_service/data/tokenissuance ...')
    client.subscribe(topicMobileBasepath + 'platform_service/data/tokenissuance')

    while tokenissuance is None:
        time.sleep(0.1)

    t = tokenissuance.payload.decode()
    t2 = json.loads(t)
    t2['client_id'] = clientID
    t2['username'] = username
    t2['password'] = fourthHash
    pprint(t2)

    print('token issued...well done!')

    json.dump(t2, open('credentials.json', 'w'))
    print('credentials saved to credentials.json')

    client.loop_stop()
    client.disconnect()

if __name__ == "__main__":
    write_token_to_creds_file()

refresh_token.py:

import paho.mqtt.client as mqtt
import time
import json
from generate_token import write_token_to_creds_file

tv_ip =         "192.168.65.61"
certfile =      "./rcm_certchain_pem.cer"
keyfile =       "./rcm_pem_privkey.pkcs8"

def load_or_generate_creds(rec=False):
    global oldCreds
    try:
        file = open('credentials.json')
    except FileNotFoundError:
        if not rec:
            print('No stored credentials found, starting auth with TV...')
            write_token_to_creds_file()
            load_or_generate_creds(True)
        else:
            print('Unable to generate credentials.')

    else:
        with file:
            oldCreds = json.load(file)

load_or_generate_creds()
refreshtoken = oldCreds["refreshtoken"]
client_id = oldCreds['client_id']
username = oldCreds['username']
password = oldCreds['password']

credentials = ""

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        client.connected_flag=True #set flag
        print("connected ok")
        # Subscribing in on_connect() means that if we lose the connection and
        # reconnect then subscriptions will be renewed.
        client.subscribe("#")
def on_message(client, userdata, message):
    print("message received " ,str(message.payload.decode("utf-8")))
    global credentials
    credentials = json.loads(message.payload.decode("utf-8"))
def on_publish(client, userdata, mid):
    print("Published message " + str(mid))
def on_disconnect(client, userdata, rc):
    print("disconnecting reason  "  +str(rc))

def refresh_token():

    client = mqtt.Client(client_id=client_id, clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp")

    client.tls_set(ca_certs=None, certfile=certfile, keyfile=keyfile, cert_reqs=mqtt.ssl.CERT_NONE,
        tls_version=mqtt.ssl.PROTOCOL_TLS, ciphers=None)

    client.on_connect = on_connect
    client.on_message = on_message
    # client.on_subscribe = on_subscribe
    client.on_publish = on_publish
    client.on_disconnect = on_disconnect

    client.connected_flag=False

    client.tls_insecure_set(True)

    client.username_pw_set(username=username, password=password)

    client.connect_async(tv_ip, 36669, 60)
    client.loop_start()

    while not client.connected_flag: #wait in loop
        print("In wait loop")
        time.sleep(1)

    client.subscribe("/remoteapp/mobile/" + client_id + "/platform_service/data/tokenissuance")
    client.publish("/remoteapp/tv/platform_service/" + client_id + "/data/gettoken", '{"refreshtoken": "' + refreshtoken + '"}')

    while credentials == "":
        print("waiting for refreshed credentials...")
        time.sleep(1)

    credentials['client_id'] = client_id
    credentials['username'] = username
    credentials['password'] = password
    with open("credentials.json", "w") as file:
        json.dump(credentials, file, indent= 4)

    print("got new credentials, terminating...")
    client.loop_stop()
    client.disconnect()

    return credentials["accesstoken"]
    #exit()

if __name__ == "__main__":
    refresh_token()
stevepbuk commented 6 months ago

Thats great thanks. I worked out what my problem was. I had been sending the password instead of the access key in the password field. Now got the same issue you mention in that I cannot subscribe using a wildcard #. Thats fine though as the spreadsheet you mention has all of the useful ones in. I found some more in the source code of this plugin.

https://github.com/sehaas/ha_hisense_tv

Thanks

Steve

SodaSurfer commented 5 months ago

Hi guys so I'm a bit confused with this topic, is it possible or not, to integrate HomaAssitance now with your scripts and the new hisense vidaa OS? can we implement it like a generic library to HomaAssitance ?

stevepbuk commented 5 months ago

So I have been playing around with a script that initially gets the token etc and then one that runs every 12 hour to refresh it. I cant the refresh to work consistently. Sometimes it just fails to get a response. I'm not sure what's going on. I need to find more time to play around.

Even if the scripts were sorted ( and I think they work for most ), someone would have to spend the time building it into some existing Hisense TV plugin in Home Assistant. As far as I know, no one has done that yet. Its definitely possible though.

I just wish Hisense would make the TV a bit simpler to connect into Home Automation. Every other major manufacturer appears to be doing this. I cant even get DLNA working consistently. My 8 year old Sony TV supported this without issue.

nikagl commented 5 months ago

I have combined all scripts into one script (that could also be used for other home automation apps and manual scripts) and tried adding getting the state and power cycling the tv to it as well. I don't think anyone tried that using python from what I can see in the scripts? For me it doesn't work, any thoughts on what I am doing wrong here?

import re
import uuid
import hashlib
import time
import json
import logging
import paho.mqtt.client as mqtt
from pprint import pprint

# Configuration
tv_ip = "192.168.178.134"
certfile = "./rcm_certchain_pem.cer"
keyfile = "./rcm_pem_privkey.pkcs8"
check_interval = 0.1

# Logging setup
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class TVAuthenticator:
    def __init__(self):
        self.reply = None
        self.authentication_payload = None
        self.authentication_code_payload = None
        self.tokenissuance = None
        self.credentials = None
        self.saved_credentials = None
        self.accesstoken = None
        self.accesstoken_time = None
        self.accesstoken_duration_day = None
        self.refreshtoken = None
        self.refreshtoken_time = None
        self.refreshtoken_duration_day = None
        self.client_id = None
        self.username = None
        self.password = None

        self.topicTVUIBasepath = None
        self.topicTVPSBasepath = None
        self.topicMobiBasepath = None
        self.topicBrcsBasepath = None
        self.topicRemoBasepath = None
        self.tv_state = None

    @staticmethod
    def cross_sum(n):
        return sum(int(digit) for digit in str(n))

    @staticmethod
    def string_to_hash(input_str):
        return hashlib.md5(input_str.encode("utf-8")).hexdigest().upper()

    def on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            client.connected_flag = True
            logging.info("Connected to MQTT broker")
            client.subscribe([
                (self.topicTVUIBasepath + "actions/vidaa_app_connect", 0),
                (self.topicMobiBasepath + 'ui_service/data/authentication', 0),
                (self.topicMobiBasepath + 'ui_service/data/authenticationcode', 0),
                (self.topicBrcsBasepath + "ui_service/data/hotelmodechange", 0),
                (self.topicMobiBasepath + 'platform_service/data/tokenissuance', 0),
                (self.topicBrcsBasepath + 'ui_service/state', 0),
            ])
        else:
            logging.error(f"Bad connection. Returned code: {rc}")
            client.bad_connection_flag = True

    def on_message(self, client, userdata, msg):
        logging.info(f"Message received: {msg.payload.decode('utf-8')} on topic {msg.topic}")
        self.reply = msg

    def on_subscribe(self, client, userdata, mid, granted_qos):
        logging.info(f"Subscribed: {mid} {granted_qos}")

    def on_publish(self, client, userdata, mid):
        logging.info(f"Published message {mid}")

    def on_disconnect(self, client, userdata, rc):
        logging.info(f"Disconnected. Reason: {rc}")

    def on_authentication(self, mosq, obj, msg):
        logging.info(f"Authentication message received: {msg.payload.decode('utf-8')} on topic {msg.topic}")
        self.authentication_payload = msg

    def on_authentication_code(self, mosq, obj, msg):
        logging.info(f"Authentication code message received: {msg.payload.decode('utf-8')} on topic {msg.topic}")
        self.authentication_code_payload = msg

    def on_tokenissuance(self, mosq, obj, msg):
        logging.info(f"Token issuance message received: {msg.payload.decode('utf-8')} on topic {msg.topic}")
        self.tokenissuance = msg

    def on_tv_state(self, mosq, obj, msg):
        logging.info(f"TV State message received: {msg.payload.decode('utf-8')} on topic {msg.topic}")
        self.tv_state = msg.payload.decode('utf-8')

    def wait_for_message(self, condition):
        time.sleep(1)  # Initial delay to prevent false negatives
        while condition():
            time.sleep(check_interval)

    def create_mqtt_client(self, client_id, certfile, keyfile, username, password, userdata=None):
        logging.info("Creating MQTT client...")
        client = mqtt.Client(client_id=client_id, clean_session=True, userdata=userdata, protocol=mqtt.MQTTv311, transport="tcp")
        client.tls_set(ca_certs=None, certfile=certfile, keyfile=keyfile, cert_reqs=mqtt.ssl.CERT_NONE, tls_version=mqtt.ssl.PROTOCOL_TLS)
        client.tls_insecure_set(True)
        client.username_pw_set(username=username, password=password)

        # Attach event handlers
        client.on_connect = self.on_connect
        client.on_message = self.on_message
        client.on_publish = self.on_publish
        client.on_disconnect = self.on_disconnect

        client.connected_flag = False
        client.bad_connection_flag = False

        return client

    def refresh_token(self):
        current_time = time.time()
        expiration_time = int(self.accesstoken_time) + (int(self.accesstoken_duration_day) * 24 * 60 * 60)

        if current_time <= expiration_time:
            logging.info("Token still valid, no need to refresh")
            time_diff = expiration_time - current_time
            days = time_diff // (24 * 60 * 60)
            hours = (time_diff % (24 * 60 * 60)) // (60 * 60)
            minutes = (time_diff % (60 * 60)) // 60
            seconds = time_diff % 60
            logging.info(f"Token expires in {int(days)} days, {int(hours)} hours, {int(minutes)} minutes, and {int(seconds)} seconds")
            logging.info(f"Token expires at {time.ctime(expiration_time)}")
            return self.accesstoken

        logging.info("Token not valid, refreshing the token")

        client = self.create_mqtt_client(client_id=self.client_id, certfile=certfile, keyfile=keyfile, username=self.username, password=self.password)
        client.connect_async(tv_ip, 36669, 60)
        client.loop_start()

        while not client.connected_flag:
            logging.info("Waiting for connection...")
            time.sleep(1)

        client.publish(f"/remoteapp/tv/platform_service/{self.client_id}/data/gettoken", json.dumps({"refreshtoken": self.refreshtoken}))

        while self.credentials is None:
            logging.info("Waiting for refreshed credentials...")
            time.sleep(1)

        self.credentials.update({"client_id": self.client_id, "username": self.username, "password": self.password})
        with open("credentials.json", "w") as file:
            json.dump(self.credentials, file, indent=4)

        logging.info("Got new credentials, terminating...")
        client.loop_stop()
        client.disconnect()

        return self.credentials["accesstoken"]

    def write_token_to_creds_file(self):
        timestamp = int(time.time())
        mac = ':'.join(re.findall('..', '%012x' % uuid.getnode())).upper()
        logging.info(f'MAC Address: {mac}')

        first_hash = self.string_to_hash("&vidaa#^app")
        second_hash = self.string_to_hash(f"38D65DC30F45109A369A86FCE866A85B${mac}")
        last_digit_of_cross_sum = self.cross_sum(timestamp) % 10
        third_hash = self.string_to_hash(f"his{last_digit_of_cross_sum}h*i&s%e!r^v0i1c9")
        fourth_hash = self.string_to_hash(f"{timestamp}${third_hash[:6]}")

        logging.info(f'First Hash: {first_hash}')
        logging.info(f'Second Hash: {second_hash}')
        logging.info(f'Third Hash: {third_hash}')
        logging.info(f'Fourth Hash: {fourth_hash}')

        client_id = f"{mac}$his${second_hash[:6]}_vidaacommon_001"
        logging.info(f'Client ID: {client_id}')

        self.topicTVUIBasepath = f"/remoteapp/tv/ui_service/{client_id}/"
        self.topicTVPSBasepath = f"/remoteapp/tv/platform_service/{client_id}/"
        self.topicMobiBasepath = f"/remoteapp/mobile/{client_id}/"
        self.topicBrcsBasepath = f"/remoteapp/mobile/broadcast/"
        self.topicRemoBasepath = f"/remoteapp/tv/remote_service/{client_id}/"

        client = self.create_mqtt_client(client_id=client_id, certfile=certfile, keyfile=keyfile, username=f"his${timestamp}", password=fourth_hash)
        client.message_callback_add(self.topicMobiBasepath + 'ui_service/data/authentication', self.on_authentication)
        client.message_callback_add(self.topicMobiBasepath + 'ui_service/data/authenticationcode', self.on_authentication_code)
        client.message_callback_add(self.topicBrcsBasepath + 'ui_service/data/hotelmodechange', self.on_message)
        client.message_callback_add(self.topicMobiBasepath + 'platform_service/data/tokenissuance', self.on_tokenissuance)

        client.connect_async(tv_ip, 36669, 60)
        client.loop_start()

        self.wait_for_message(lambda: not client.connected_flag)

        logging.info('Publishing message to actions/vidaa_app_connect...')
        client.publish(self.topicTVUIBasepath + "actions/vidaa_app_connect", '{"app_version":2,"connect_result":0,"device_type":"Mobile App"}')
        self.wait_for_message(lambda: self.authentication_payload is None)

        if self.authentication_payload.payload.decode() != '""':
            logging.error('Problems with the authentication message!')
            logging.error(self.authentication_payload.payload)
            return

        logging.info(f'Subscribing to {self.topicMobiBasepath}ui_service/data/authenticationcode...')
        client.subscribe(self.topicMobiBasepath + 'ui_service/data/authenticationcode')

        authsuccess = False
        while not authsuccess:
            auth_num = input("Enter the four digits displayed on your TV: ")
            client.publish(self.topicTVUIBasepath + "actions/authenticationcode", f'{{"authNum":{auth_num}}}')

            self.wait_for_message(lambda: self.authentication_code_payload is None)

            if json.loads(self.authentication_code_payload.payload.decode()) != {"result": 1, "info": ""}:
                logging.error('Problems with the authentication message!')
                logging.error(self.authentication_code_payload.payload)
            else:
                authsuccess = True

        logging.info("Success! Getting access token...")
        client.publish(self.topicTVPSBasepath + "data/gettoken", '{"refreshtoken": ""}')
        client.publish(self.topicTVUIBasepath + "actions/authenticationcodeclose")

        client.subscribe(self.topicBrcsBasepath + 'ui_service/data/hotelmodechange')
        client.subscribe(self.topicMobiBasepath + 'platform_service/data/tokenissuance')

        self.wait_for_message(lambda: self.tokenissuance is None)

        token_data = json.loads(self.tokenissuance.payload.decode())
        token_data.update({"client_id": client_id, "username": f"his${timestamp}", "password": fourth_hash})
        pprint(token_data)

        logging.info('Token issued successfully')
        with open('credentials.json', 'w') as file:
            json.dump(token_data, file)
        logging.info('Credentials saved to credentials.json')

        client.loop_stop()
        client.disconnect()

    def load_or_generate_creds(self, rec=False):
        try:
            with open('credentials.json', 'r') as file:
                self.saved_credentials = json.load(file)
                self.accesstoken = self.saved_credentials["accesstoken"]
                self.accesstoken_time = self.saved_credentials["accesstoken_time"]
                self.accesstoken_duration_day = self.saved_credentials["accesstoken_duration_day"]
                self.refreshtoken = self.saved_credentials["refreshtoken"]
                self.refreshtoken_time = self.saved_credentials["refreshtoken_time"]
                self.refreshtoken_duration_day = self.saved_credentials["refreshtoken_duration_day"]
                self.client_id = self.saved_credentials['client_id']
                self.username = self.saved_credentials['username']
                self.password = self.saved_credentials['password']

                self.topicTVUIBasepath = f"/remoteapp/tv/ui_service/{self.client_id}/"
                self.topicTVPSBasepath = f"/remoteapp/tv/platform_service/{self.client_id}/"
                self.topicMobiBasepath = f"/remoteapp/mobile/{self.client_id}/"
                self.topicBrcsBasepath = f"/remoteapp/mobile/broadcast/"
                self.topicRemoBasepath = f"/remoteapp/tv/remote_service/{self.client_id}/"
        except FileNotFoundError:
            if not rec:
                logging.info('No stored credentials found, starting auth with TV...')
                self.write_token_to_creds_file()
                self.load_or_generate_creds(True)
            else:
                logging.error('Unable to generate credentials.')
                raise

    # Action: Get TV State
    # Publish: /remoteapp/tv/ui_service/36:BD:7A:BA:29:16$his$9D286C_vidaacommon_001/actions/gettvstate
    # Subscribe: /remoteapp/mobile/broadcast/ui_service/state
    # Response example: {"statetype":"remote_launcher"}

    def get_tv_state(self):
        client = self.create_mqtt_client(client_id=self.client_id, certfile=certfile, keyfile=keyfile, username=self.username, password=self.password)
        client.message_callback_add(self.topicBrcsBasepath + 'ui_service/state', self.on_tv_state)
        client.connect_async(tv_ip, 36669, 60)
        client.loop_start()

        while not client.connected_flag:
            logging.info("Waiting for connection...")
            time.sleep(1)

        logging.info(f"Publishing get TV state message to {self.topicTVUIBasepath}actions/gettvstate")
        client.publish(self.topicTVUIBasepath + "actions/gettvstate")

        self.wait_for_message(lambda: self.tv_state is None)

        if self.tv_state:
            logging.info(f"TV State received: {self.tv_state}")
            return json.loads(self.tv_state)
        else:
            logging.error("Failed to get TV state")
            return None

    # Action: Power Cycle
    # Publish: /remoteapp/tv/remote_service/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/actions/sendkey
    # Message: KEY_POWER

    def power_cycle_tv(self):
        client = self.create_mqtt_client(client_id=self.client_id, certfile=certfile, keyfile=keyfile, username=self.username, password=self.password)
        client.connect_async(tv_ip, 36669, 60)
        client.loop_start()

        while not client.connected_flag:
            logging.info("Waiting for connection...")
            time.sleep(1)

        logging.info(f"Publishing power cycle message to {self.topicRemoBasepath}actions/sendkey")
        client.publish(self.topicRemoBasepath + "actions/sendkey", "KEY_POWER")

        logging.info("Power cycle command sent.")
        client.loop_stop()
        client.disconnect()

if __name__ == "__main__":
    # Initialize the TVAuthenticator class
    auth = TVAuthenticator()

    # Load or generate credentials
    auth.load_or_generate_creds()

    # Refresh the token if needed
    auth.refresh_token()

    # Power cycle the TV
    auth.power_cycle_tv()

    # Get TV State
    tv_state = auth.get_tv_state()
    if tv_state:
        logging.info(f"TV State: {tv_state}")

# Action: Get Source List
# Publish: /remoteapp/tv/ui_service/36:BD:7A:BA:29:16$his$9D286C_vidaacommon_001/actions/sourcelist
# Subscribe: /remoteapp/mobile/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/ui_service/data/sourcelist
# Response example: [{"sourceid":"vidaahome","sourcename":"Home","displayname":"Home","displayname2":"","httpIcon":"","is_signal":"1","has_signal":"1","is_lock":"0","hotel_mode":"0"},{"sourceid":"TV","sourcename":"TV","displayname":"TV","displayname2":"","httpIcon":"","is_signal":"0","has_signal":"0","is_lock":"0","hotel_mode":"0"},{"sourceid":"284","sourcename":"VIDAA tv","displayname":"VIDAA tv","displayname2":"","httpIcon":"https://img.vidaahub.com/vidaa/2022/7/202207190701576882.png","is_signal":"0","has_signal":"1","is_lock":"0","hotel_mode":"0"},{"sourceid":"HDMI1","sourcename":"HDMI1","displayname":"Apple TV","displayname2":"Wohnzimmer","httpIcon":"","is_signal":"0","has_signal":"0","is_lock":"0","hotel_mode":"0"},{"sourceid":"HDMI2","sourcename":"HDMI2","displayname":"HDMI2","displayname2":"","httpIcon":"","is_signal":"0","has_signal":"0","is_lock":"0","hotel_mode":"0"},{"sourceid":"HDMI3","sourcename":"HDMI3","displayname":"PS5","displayname2":"PlayStation 5","httpIcon":"","is_signal":"0","has_signal":"0","is_lock":"0","hotel_mode":"0"},{"sourceid":"HDMI4","sourcename":"HDMI4","displayname":"PC","displayname2":"","httpIcon":"","is_signal":"0","has_signal":"0","is_lock":"0","hotel_mode":"0"},{"sourceid":"AVS","sourcename":"AV","displayname":"AV","displayname2":"","httpIcon":"","is_signal":"0","has_signal":"0","is_lock":"0","hotel_mode":"0"},{"sourceid":"MusicSharing","sourcename":"Music Sharing","displayname":"Music Sharing","displayname2":"","httpIcon":"","is_signal":"0","has_signal":"1","is_lock":"0","hotel_mode":"0"}]

# Action: Change Source
# Publish: /remoteapp/tv/ui_service/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/actions/changesource
# Message: {"sourceid":"HDMI3"}

# Action: Get Volume
# Publish: /remoteapp/tv/platform_service/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/actions/getvolume
# Subscribe: /remoteapp/mobile/broadcast/ui_service/volume

# Action: Change Volume
# Publish: /remoteapp/tv/platform_service/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/actions/changevolume
# Subscribe: /remoteapp/mobile/broadcast/ui_service/volume
# Message: 0-100

# Action: Get App List
# Publish: /remoteapp/tv/ui_service/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/actions/applist
# Subscribe: /remoteapp/mobile/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/ui_service/data/applist
# Response example: [{"url":"https://app.plex.tv/tv-vidaa","isunInstalled":false,"name":"Plex","from":"","storeType":98,"appId":"42","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090915047445.png"},{"url":"netflix","isunInstalled":false,"name":"Netflix","from":"","storeType":98,"appId":"1","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090857551807.png"},{"url":"youtube","isunInstalled":false,"name":"YouTube","from":"","storeType":98,"appId":"3","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090858299321.png"},{"url":"https://cd-dmgz.bamgrid.com/bbd/hisense_tv/index.html","isunInstalled":false,"name":"Disney+","from":"","storeType":99,"appId":"295","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090928459990.png"},{"url":"https://atve.tv.apple.com/94819831-5404-4438-810e-afb648d6a826/tvw_1dc2aab0a313427dbc11818be8a18bd9/","isunInstalled":false,"name":"Apple TV+","from":"","storeType":99,"appId":"351","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2022/1/202201280255184463.png"},{"url":"amazon","isunInstalled":false,"name":"Prime Video","from":"","storeType":98,"appId":"2","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090859207066.png"},{"url":"https://api-portal-v3.netrange.com/portal/v3/appredir?PrKey=645e94eb48044bb07a88090c250b472f842d77ee&AppId=190","isunInstalled":false,"name":"ARD Mediathek","from":"","storeType":93,"appId":"crawler_60061_nrmmh-190","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060832013925.jpg"},{"url":"http://hbbtv.zdf.de/zdfm3/index.php?html5=1&portal=1&live=1","isunInstalled":false,"name":"ZDF","from":"","storeType":97,"appId":"187","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090927051313.png"},{"url":"https://tv.client.ott.sky.com/?MSoQ8ri2lztP50gc1YwprgPtZT3PkP&territory=DE","isunInstalled":false,"name":"WOW TV","from":"","storeType":99,"appId":"2201","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/12/202312131406047409.png?resize=w_300,h_300"},{"url":"https://app.hisense.rtl-smart.tv/","isunInstalled":false,"name":"RTL TVNOW","from":"","storeType":97,"appId":"300","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090925176358.png"},{"url":"vidaa-store","isunInstalled":false,"name":"APP STORE","from":"","storeType":99,"appId":"164","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210659518385.png"},{"url":"https://www.intl.paramountplus.com/smart-console-apps/vidaa/","isunInstalled":false,"name":"Paramount+","from":"","storeType":99,"appId":"334","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2022/5/202205090739158843.png"},{"url":"browser","isunInstalled":false,"name":"TV Browser","from":"","storeType":100,"appId":"16","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210704287492.png"},{"url":"https://prod-hisense-ui40-app.rakuten.tv/","isunInstalled":false,"name":"Rakuten TV","from":"","storeType":99,"appId":"65","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090900492844.png"},{"url":"https://zattoo-vidaa.zattoo.com","isunInstalled":false,"name":"ZATTOO","from":"","storeType":99,"appId":"1748","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060834575429.png"},{"url":"https://tv.dazn.com/app/vidaa/","isunInstalled":false,"name":"DAZN","from":"","storeType":99,"appId":"162","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090902316899.png"},{"url":"https://tvmodules-vidaa.vidaahub.com/publicvalue/V1.0/index-publicvalue.html","isunInstalled":false,"name":"Public value","from":"","storeType":99,"appId":"2081","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210713198850.png"},{"url":"https://app-hisense.pluto.tv/","isunInstalled":false,"name":"PlutoTV","from":"","storeType":98,"appId":"173","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090917583451.png"},{"url":"https://tv2.deezer.com/hisense","isunInstalled":false,"name":"Deezer","from":"","storeType":98,"appId":"47","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090916047381.png"},{"url":"https://live.prd.ott.s.joyn.de/?platform=hisense","isunInstalled":false,"name":"JOYN","from":"","storeType":99,"appId":"1908","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060836507140.jpg"},{"url":"https://hisense.chili.com","isunInstalled":false,"name":"Chili","from":"","storeType":98,"appId":"73","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090913449937.png"},{"url":"hdplus","isunInstalled":false,"name":"HD+ LIVE TV","from":"","storeType":98,"appId":"325","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060833285324.png"},{"url":"https://dn6q4f0q0kh23.cloudfront.net/receiver/hisense/haystackreceiver.html","isunInstalled":false,"name":"Haystack TV Local + World News","from":"","storeType":98,"appId":"227","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090918322670.png"},{"url":"https://hisense2-1080.rlaxxtv.com/","isunInstalled":false,"name":"RLAXX","from":"","storeType":99,"appId":"279","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090921287664.png"},{"url":"https://ctv-vidaa.kidoodle.tv","isunInstalled":false,"name":"Kidoodle.TV","from":"","storeType":98,"appId":"233","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090917185351.png"},{"url":"https://maxdomesmarttv3.meinvod.de/","isunInstalled":false,"name":"Maxdome","from":"","storeType":98,"appId":"143","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060838581821.png"},{"url":"https://france24.tv.dotscreen.com/hisense/index.html?languages=EN,FR,ES,AR","isunInstalled":false,"name":"France24","from":"","storeType":99,"appId":"372","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2022/2/202202070800239017.png"},{"url":"https://hisense.vevo.com","isunInstalled":false,"name":"VEVO","from":"","storeType":98,"appId":"306","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060832526783.png"},{"url":"https://hisense.plus.fifa.com/","isunInstalled":false,"name":"FIFA+","from":"","storeType":99,"appId":"1714","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060834007733.png"},{"url":"https://ott-apps.fite.tv/hisense/index.html","isunInstalled":false,"name":"FITE - Boxing, Wrestling, MMA","from":"","storeType":99,"appId":"245","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090919469239.png"},{"url":"https://smarttv.uefa.tv","isunInstalled":false,"name":"UEFA.tv","from":"","storeType":98,"appId":"244","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090919094943.png"},{"url":"https://smarttv3.videociety.de","isunInstalled":false,"name":"Videociety","from":"","storeType":98,"appId":"78","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090912274669.png"},{"url":"youtube_kids","isunInstalled":false,"name":"YouTube Kids","from":"","storeType":98,"appId":"38","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090911395201.png"},{"url":"https://vidaa.servustv.com/","isunInstalled":false,"name":"ServusTV","from":"","storeType":98,"appId":"323","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090927409428.png"},{"url":"https://vidaa.redbull.tv/","isunInstalled":false,"name":"Red Bull TV","from":"","storeType":98,"appId":"79","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090916463905.png"},{"url":"https://html5tv.music.amazon.dev/?deviceModel=A27F3L5WMHDOWD","isunInstalled":false,"name":"Amazon Music","from":"","storeType":99,"appId":"376","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060839281117.png"},{"url":"vidaa-plus","isunInstalled":false,"name":"VIDAA tv","from":"","storeType":98,"appId":"284","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210736143340.png"},{"url":"vidaa-art","isunInstalled":false,"name":"VIDAA Art","from":"","storeType":98,"appId":"185","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210729076057.png"},{"url":"screensaver","isunInstalled":false,"name":"Screensaver","from":"","storeType":98,"appId":"1672","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210727367557.png"},{"url":"vidaa-free","isunInstalled":false,"name":"VIDAA Free","from":"","storeType":98,"appId":"184","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210732337444.png"},{"url":"https://erosnowhtml.erosnow.com/main.html?partner_code=VIDD","isunInstalled":false,"name":"Eros Now","from":"","storeType":98,"appId":"178","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090924148666.png"},{"url":"accuweather","isunInstalled":false,"name":"AccuWeather","from":"","storeType":98,"appId":"15","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311091029582894.png"},{"url":"https://cweb-ott.nba.com/","isunInstalled":false,"name":"NBA","from":"","storeType":99,"appId":"293","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2022/9/202209160010272173.png"},{"url":"http://apps.tvgam.es/tv_games/hisense_portal/production/portal/index.html","isunInstalled":false,"name":"Game Center","from":"","storeType":98,"appId":"35","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090915362271.png"},{"url":"https://smarttv.molotov.tv/","isunInstalled":false,"name":"MOLOTOV.TV","from":"","storeType":99,"appId":"287","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090922406418.png"},{"url":"https://tv-production.mubi.com/vidaa/4.0.0/","isunInstalled":false,"name":"MUBI","from":"","storeType":98,"appId":"230","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2022/4/202204300235256422.png"},{"url":"https://html5.toongoggles.com/","isunInstalled":false,"name":"Toon Goggles","from":"","storeType":99,"appId":"36","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311060906446498.png"},{"url":"https://vewd.global.united.cloud/","isunInstalled":false,"name":"EON TV","from":"","storeType":99,"appId":"280","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090922112374.png"},{"url":"https://dice-tv.imggaming.com/vidaa/dce.ufc/index.html","isunInstalled":false,"name":"UFC","from":"","storeType":99,"appId":"355","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090930102217.png"},{"url":"https://prod.hisense.hisense.vidmind.cc/","isunInstalled":false,"name":"Kyivstar TV","from":"","storeType":98,"appId":"288","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090923183570.png"},{"url":"file:///APPS/emanual/Local_ebook/index.html","isunInstalled":false,"name":"E-Manual","from":"","storeType":99,"appId":"166","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311210706069065.png"},{"url":"file:///APPS/emanual/Local_ebook/index.html?fromTransparency=1","isunInstalled":false,"name":"Richtlinien","from":"","storeType":99,"appId":"296","move":true,"isFav":true,"httpIcon":"data:image/png;base64,iconDownloadhttps://img.vidaahub.com/vidaa/2023/11/202311090924486903.png"}]

# Action: Launch App Netflix
# Publish: /remoteapp/tv/ui_service/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/actions/launchapp
# Message: { "url": "netflix" }

# Action: Launch App Youtube
# Publish: /remoteapp/tv/ui_service/C1:BE:D1:3D:6E:3E$his$7E1991_vidaacommon_001/actions/launchapp
# Message: { "url" : "youtube" }