#
# Connects to power pet servers via wss MQTT
#     Script uses keyboard, has to be run from command line
#
# Findings
#   There are 2 MQTT servers, the one i connect to might not be the one the door connected to,
#     if hitting the number 5 does not result in a response, disconnect and reconnect and try again
#
#   Only stays connected for 10 minutes... then their server disconnects, even when sending commands
#     looks like even the door has to reconnect every 10 minutes,  bue to the first finding this means the door functionaliy can break every 10 minutes
#
#   I bet there is a firmware update option here somewhere.  I could create a python script to pretend to be a door with old firmware
#        My guess is that the server responds with a URL to download firmware from
#        Maybe i could decompile that and change the WSS MQTT server to my own
#
#   Possible fix might be to create a mqtt server in the house on an IP using port 442
#     Then hijack the DNS for iot.hightechpet.com and send it to my server77
#
#
#Commands i have seen go by and tested.
#client.publish(sub_topic,'{"config":"POWER_ON","msgId":4201,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"POWER_OFF","msgId":4202,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"DISABLE_TIMERS","msgId":4203,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"ENABLE_TIMERS","msgId":4204,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"DISABLE_INSIDE","msgId":4205,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"ENABLE_INSIDE","msgId":4206,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"DISABLE_OUTSIDE","msgId":4207,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"ENABLE_OUTSID","msgId":4208,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_DOOR_STATUS","msgId":4209,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_HW_INFO","msgId":4210,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_POWER","msgId":4211,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_SENSORS","msgId":4211,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_DOOR_BATTERY","msgId":4212,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"CHECK_RESET_REASON","msgId":4213,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_SCHEDULE_LIST","msgId":4214,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_SETTINGS","msgId":4214,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"HAS_REMOTE_ID","msgId":4215,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"HAS_REMOTE_KEY","msgId":4215,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"config":"GET_DOOR_BATTERY","msgId":4216,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"cmd":"OPEN_AND_HOLD","msgId":4217,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"cmd":"CLOSE","msgId":4217,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"cmd":"OPEN","msgId":4218,"dir":"p2d"}',1,False)
#client.publish(sub_topic,'{"PING":"9082340589","dir":"p2d"}',1,False)

#Events seen when Door opening
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":0,"door_status":"DOOR_RISING"}
#message received   {"success":"true","dir":"d2p","CMD":"OPEN"}
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":960,"door_status":"DOOR_SLOWING"}
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":1630,"door_status":"DOOR_HOLDING"}
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":1630,"door_status":"DOOR_CLOSING"}
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":1870,"door_status":"DOOR_CLOSING_TOP_OPEN"}
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":3380,"door_status":"DOOR_CLOSING_MID_OPEN"}
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":4440,"door_status":"DOOR_CLOSED"}
#message received   {"dir":"d2p","CMD":"DOOR_STATUS","success":"true","delta":4440,"door_status":"DOOR_IDLE"}
#
#when getting a packet back look at "dir", "d2p" is a response from the door "p2d" is something talking to the door.
#
#


#***********************************************************************************
#* The only variables to edit, it needs an account username/password
#* if left blank, it will prompt for them 
#***********************************************************************************
EMAIL = ""
PASSWORD = ""

#***********************************************************************************

if (EMAIL==''):
   EMAIL = input("Enter account Email: ")
if (PASSWORD==''):
   PASSWORD = input("Enter account Password: ")

import paho.mqtt.client as paho
import time
import ssl
from datetime import datetime
import msvcrt 
import threading

import json
import requests
from requests.structures import CaseInsensitiveDict

broker="iot.hightechpet.com"
port=443


#***********************************************************************************
#* The 3 HTTP API's that need to be used to get our MQTT Topic
#***********************************************************************************
def GetRefreshTokenJSON(Email, Password):
    r = requests.post("https://app.hightechpet.com/api/login", data={'Email': Email, 'Password': Password}, headers = {'Accept': 'application/json','Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8','Bearer': ''})
    return json.loads(r.text)
   
def GetAuthKeyJSON(RefreshToken):
   headers = CaseInsensitiveDict()
   headers["Accept"] = "application/json"
   headers["Authorization"] = "RefreshToken " + RefreshToken
   r = requests.get("https://app.hightechpet.com/api/refreshToken", headers = headers)
   return json.loads(r.text)

def GetPetDoorJSON(APIKey):
   headers = CaseInsensitiveDict()
   headers["Accept"] = "application/json"
   headers["Authorization"] = "Bearer " + APIKey
   r = requests.get("https://app.hightechpet.com/api/petdoors/", headers = headers)
   return json.loads(r.text)

#***********************************************************************************
#* MQTT Call Backs
#***********************************************************************************
def on_subscribe(client, userdata, mid, granted_qos):   #create function for callback
   print("subscribed with qos",granted_qos,"\n")
   client.subscribed_flag=True #set flag
   pass
def on_message(client, userdata, message):
   now = datetime.now().time() # time object
   print(now, "Rec:"  ,str(message.payload.decode("utf-8")))
def on_publish(client,userdata,mid):   #create function for callback
   #print("data published mid=",mid, "\n")
   pass
def on_disconnect(client, userdata, rc):
   print("client disconnected ok")
   client.connected_flag=False #set flag
   now = datetime.now().time() # time object
   print("Disconnect =", now)
def ssl_alpn():
    try:
        #debug print opnessl version
        ssl_context = ssl.create_default_context()
        return  ssl_context
    except Exception as e:
        print("exception ssl_alpn()")
        raise e
def on_connect(client, userdata, flags, rc):
    if rc==0:
        client.connected_flag=True #set flag
        print("connected OK")
    else:
        print("Bad connection Returned code=",rc)

#***********************************************************************************
#* Keyboard Input
#***********************************************************************************
def kbfunc(): 
   if msvcrt.kbhit(): 
      ret = ord(msvcrt.getch())
   else: 
      ret = ''
   return ret

#Use Get and Post to access the API to get our Channel Key and ID that we need for the MQTT connection
RefreshTokenJSON = GetRefreshTokenJSON(EMAIL, PASSWORD)
RefreshToken = RefreshTokenJSON["refresh_token"]
AuthKeyJSON = GetAuthKeyJSON(RefreshToken)
AuthKey = AuthKeyJSON["auth_token"]
PetDoorJSON = GetPetDoorJSON(AuthKey)
sub_topic=str(PetDoorJSON[0]["channelKey"]) + "/" + str(PetDoorJSON[0]["id"]) + "/ctrl/"

#Start a connection to the WSS server
client= paho.Client("client-socks",transport='websockets')       #create client object

#Hook our events to call backs
client.on_subscribe = on_subscribe       
client.on_publish = on_publish
client.on_message = on_message
client.on_disconnect = on_disconnect
client.on_connect=on_connect

#server uses SSL so we need an SSL Context
ssl_context= ssl_alpn()
client.tls_set_context(context=ssl_context)

#the hardcoded username for this server is "Support", no password, no certificates, nothing... 
client.username_pw_set(username="Support")

#Default the connected flag to false (disconnected), this is for the auto reconnect.
client.connected_flag=False

#message ID's increment, they dont seem to really matter, the responses just come back with the same ID.
msgid=1000

#loop forever
while True:
   now = datetime.now().time() # time object
   print("Start =", now)

   print("connecting to broker ",broker,"on port ",port)
   client.connect(broker,port)           #establish connection
   client.loop_start()
   
   while not client.connected_flag: #wait in loop until connected
       print("Waiting for Connect")
       time.sleep(1)

   print("subscribing to ",sub_topic)
   client.subscribe(sub_topic)
   time.sleep(1)

   while not client.subscribed_flag: #wait in loop until subscribed
       print("Waiting for Subscribe")
       time.sleep(1)

   while client.connected_flag: #Loop while still connected, waiting for keyboard input
      keyCode = kbfunc()

      if keyCode == 49:   #1 ws hit
         client.publish(sub_topic,'{"config":"POWER_ON","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 50:   #2 ws hit
         client.publish(sub_topic,'{"config":"POWER_OFF","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 51:   #3 ws hit
         client.publish(sub_topic,'{"config":"GET_DOOR_STATUS","msgId"' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 52:   #4 ws hit
         client.publish(sub_topic,'{"cmd":"OPEN","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 53:   #5 ws hit
         #QOS article https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels/
         client.publish(sub_topic,'{"config":"GET_SCHEDULE_LIST","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 54:   #6 ws hit
         client.publish(sub_topic,'{"config":"DISABLE_TIMERS","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 55:   #7 ws hit
         client.publish(sub_topic,'{"config":"ENABLE_TIMERS","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 56:   #8 ws hit
         client.publish(sub_topic,'{"config":"ENABLE_INSIDE","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 57:   #9 ws hit
         client.publish(sub_topic,'{"cmd":"OPEN_AND_HOLD","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 111:   #o ws hit
         client.publish(sub_topic,'{"cmd":"CLOSE","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      elif keyCode == 48:   #0 ws hit
         #I think the ping is supposed to be an Int8 Date but it doesn't seem to matter.
         client.publish(sub_topic,'{"PING":"1642961701895","msgId":' + str(msgid) + ',"dir":"p2d"}',1,False)
         msgid += 1
      else:
         if keyCode:
            print("Key Hit:" + str(keyCode))
      
      time.sleep(1)

   client.loop_stop()
   client.disconnect()


