Skip to content

Commit

Permalink
add new encryption method (#165)
Browse files Browse the repository at this point in the history
* add new encryption method
  • Loading branch information
tomaszduda23 committed May 27, 2024
1 parent 44ceb52 commit ca39595
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 23 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ This component is added to HACS default repository list.
port: 7000
mac: '<mac address of your first AC. NOTE: Format can be XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX depending on your model>'
target_temp_step: 1
encryption_key: <OPTIONAL: custom encryption key>
encryption_key: <OPTIONAL: custom encryption key. Integration will try to get key from device if empty>
encryption_version: <OPTIONAL: should be set to 2 for V1.21>
uid: <some kind of device identifier. NOTE: for some devices this is optional>
temp_sensor: <entity id of the EXTERNAL temperature sensor. For example: sensor.bedroom_temperature>
lights: <OPTIONAL: input_boolean to switch AC lights mode on/off. For example: input_boolean.first_ac_lights>
Expand Down
83 changes: 61 additions & 22 deletions custom_components/gree/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
CONF_AUTO_XFAN = 'auto_xfan'
CONF_AUTO_LIGHT = 'auto_light'
CONF_TARGET_TEMP = 'target_temp'
CONF_ENCRYPTION_VERSION = 'encryption_version'

DEFAULT_PORT = 7000
DEFAULT_TIMEOUT = 10
Expand All @@ -83,6 +84,9 @@
SWING_MODES = ['Default', 'Swing in full range', 'Fixed in the upmost position', 'Fixed in the middle-up position', 'Fixed in the middle position', 'Fixed in the middle-low position', 'Fixed in the lowest position', 'Swing in the downmost region', 'Swing in the middle-low region', 'Swing in the middle region', 'Swing in the middle-up region', 'Swing in the upmost region']
PRESET_MODES = ['Default', 'Full swing', 'Fixed in the leftmost position', 'Fixed in the middle-left position', 'Fixed in the middle postion','Fixed in the middle-right position', 'Fixed in the rightmost position']

GCM_IV = b'\x54\x40\x78\x44\x49\x67\x5a\x51\x6c\x5e\x63\x13'
GCM_ADD = b'qualcomm-test'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
Expand All @@ -102,7 +106,8 @@
vol.Optional(CONF_UID): cv.positive_int,
vol.Optional(CONF_AUTO_XFAN): cv.boolean,
vol.Optional(CONF_AUTO_LIGHT): cv.boolean,
vol.Optional(CONF_TARGET_TEMP): cv.entity_id
vol.Optional(CONF_TARGET_TEMP): cv.entity_id,
vol.Optional(CONF_ENCRYPTION_VERSION, default=1): cv.positive_int,
})

async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
Expand Down Expand Up @@ -131,16 +136,17 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N
uid = config.get(CONF_UID)
auto_xfan = config.get(CONF_AUTO_XFAN)
auto_light = config.get(CONF_AUTO_LIGHT)
encryption_version = config.get(CONF_ENCRYPTION_VERSION)

_LOGGER.info('Adding Gree climate device to hass')

async_add_devices([
GreeClimate(hass, name, ip_addr, port, mac_addr, timeout, target_temp_step, temp_sensor_entity_id, lights_entity_id, xfan_entity_id, health_entity_id, powersave_entity_id, sleep_entity_id, eightdegheat_entity_id, air_entity_id, target_temp_entity_id, hvac_modes, fan_modes, swing_modes, preset_modes, auto_xfan, auto_light, encryption_key, uid)
GreeClimate(hass, name, ip_addr, port, mac_addr, timeout, target_temp_step, temp_sensor_entity_id, lights_entity_id, xfan_entity_id, health_entity_id, powersave_entity_id, sleep_entity_id, eightdegheat_entity_id, air_entity_id, target_temp_entity_id, hvac_modes, fan_modes, swing_modes, preset_modes, auto_xfan, auto_light, encryption_version, encryption_key, uid)
])

class GreeClimate(ClimateEntity):

def __init__(self, hass, name, ip_addr, port, mac_addr, timeout, target_temp_step, temp_sensor_entity_id, lights_entity_id, xfan_entity_id, health_entity_id, powersave_entity_id, sleep_entity_id, eightdegheat_entity_id, air_entity_id, target_temp_entity_id, hvac_modes, fan_modes, swing_modes, preset_modes, auto_xfan, auto_light,encryption_key=None, uid=None):
def __init__(self, hass, name, ip_addr, port, mac_addr, timeout, target_temp_step, temp_sensor_entity_id, lights_entity_id, xfan_entity_id, health_entity_id, powersave_entity_id, sleep_entity_id, eightdegheat_entity_id, air_entity_id, target_temp_entity_id, hvac_modes, fan_modes, swing_modes, preset_modes, auto_xfan, auto_light, encryption_version, encryption_key=None, uid=None):
_LOGGER.info('Initialize the GREE climate device')
self.hass = hass
self._name = name
Expand Down Expand Up @@ -182,12 +188,19 @@ def __init__(self, hass, name, ip_addr, port, mac_addr, timeout, target_temp_ste
self._preset_modes = preset_modes

self._enable_turn_on_off_backwards_compatibility = False

self.encryption_version = encryption_version

if encryption_key:
_LOGGER.info('Using configured encryption key: {}'.format(encryption_key))
self._encryption_key = encryption_key.encode("utf8")
else:
self._encryption_key = self.GetDeviceKey().encode("utf8")
if encryption_version == 1:
self._encryption_key = self.GetDeviceKey().encode("utf8")
elif encryption_version == 2:
self._encryption_key = self.GetDeviceKeyGCM().encode("utf8")
else:
_LOGGER.error('Encryption version %s is not implemented.' % encryption_version)
_LOGGER.info('Fetched device encrytion key: %s' % str(self._encryption_key))

self._auto_xfan = auto_xfan
Expand All @@ -202,8 +215,11 @@ def __init__(self, hass, name, ip_addr, port, mac_addr, timeout, target_temp_ste

self._firstTimeRun = True

# Cipher to use to encrypt/decrypt
self.CIPHER = AES.new(self._encryption_key, AES.MODE_ECB)
if encryption_version == 1:
# Cipher to use to encrypt/decrypt
self.CIPHER = AES.new(self._encryption_key, AES.MODE_ECB)
elif encryption_version != 2:
_LOGGER.error('Encryption version %s is not implemented.' % encryption_version)

if temp_sensor_entity_id:
_LOGGER.info('Setting up temperature sensor: ' + str(temp_sensor_entity_id))
Expand Down Expand Up @@ -269,6 +285,9 @@ def FetchResult(self, cipher, ip_addr, port, timeout, json):
pack = receivedJson['pack']
base64decodedPack = base64.b64decode(pack)
decryptedPack = cipher.decrypt(base64decodedPack)
if self.encryption_version == 2:
tag = receivedJson['tag']
cipher.verify(base64.b64decode(tag))
decodedPack = decryptedPack.decode("utf-8")
replacedPack = decodedPack.replace('\x0f', '').replace(decodedPack[decodedPack.rindex('}')+1:], '')
loadedJsonPack = simplejson.loads(replacedPack)
Expand All @@ -282,9 +301,35 @@ def GetDeviceKey(self):
jsonPayloadToSend = '{"cid": "app","i": 1,"pack": "' + pack + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid": 0}'
return self.FetchResult(cipher, self._ip_addr, self._port, self._timeout, jsonPayloadToSend)['key']

def GetGCMCipher(self, key):
cipher = AES.new(key, AES.MODE_GCM, nonce=GCM_IV)
cipher.update(GCM_ADD)
return cipher

def EncryptGCM(self, key, plaintext):
encrypted_data, tag = self.GetGCMCipher(key).encrypt_and_digest(plaintext.encode("utf8"))
pack = base64.b64encode(encrypted_data).decode('utf-8')
tag = base64.b64encode(tag).decode('utf-8')
return (pack, tag)

def GetDeviceKeyGCM(self):
_LOGGER.info('Retrieving HVAC encryption key')
GENERIC_GREE_DEVICE_KEY = b'{yxAHAY_Lm6pbC/<'
plaintext = '{"cid":"' + str(self._mac_addr) + '", "mac":"' + str(self._mac_addr) + '","t":"bind","uid":0}'
pack, tag = self.EncryptGCM(GENERIC_GREE_DEVICE_KEY, plaintext)
jsonPayloadToSend = '{"cid": "app","i": 1,"pack": "' + pack + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid": 0, "tag" : "' + tag + '"}'
return self.FetchResult(self.GetGCMCipher(GENERIC_GREE_DEVICE_KEY), self._ip_addr, self._port, self._timeout, jsonPayloadToSend)['key']

def GreeGetValues(self, propertyNames):
jsonPayloadToSend = '{"cid":"app","i":0,"pack":"' + base64.b64encode(self.CIPHER.encrypt(self.Pad('{"cols":' + simplejson.dumps(propertyNames) + ',"mac":"' + str(self._mac_addr) + '","t":"status"}').encode("utf8"))).decode('utf-8') + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid":{}'.format(self._uid) + '}'
return self.FetchResult(self.CIPHER, self._ip_addr, self._port, self._timeout, jsonPayloadToSend)['dat']
plaintext = '{"cols":' + simplejson.dumps(propertyNames) + ',"mac":"' + str(self._mac_addr) + '","t":"status"}'
if self.encryption_version == 1:
cipher = self.CIPHER
jsonPayloadToSend = '{"cid":"app","i":0,"pack":"' + base64.b64encode(cipher.encrypt(self.Pad(plaintext).encode("utf8"))).decode('utf-8') + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid":{}'.format(self._uid) + '}'
elif self.encryption_version == 2:
pack, tag = self.EncryptGCM(self._encryption_key, plaintext)
jsonPayloadToSend = '{"cid":"app","i":0,"pack":"' + pack + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid":{}'.format(self._uid) + ',"tag" : "' + tag + '"}'
cipher = self.GetGCMCipher(self._encryption_key)
return self.FetchResult(cipher, self._ip_addr, self._port, self._timeout, jsonPayloadToSend)['dat']

def SetAcOptions(self, acOptions, newOptionsToOverride, optionValuesToOverride = None):
if not (optionValuesToOverride is None):
Expand All @@ -304,20 +349,14 @@ def SetAcOptions(self, acOptions, newOptionsToOverride, optionValuesToOverride =
def SendStateToAc(self, timeout):
_LOGGER.info('Start sending state to HVAC')
statePackJson = '{' + '"opt":["Pow","Mod","SetTem","WdSpd","Air","Blo","Health","SwhSlp","Lig","SwingLfRig","SwUpDn","Quiet","Tur","StHt","TemUn","HeatCoolType","TemRec","SvSt","SlpMod"],"p":[{Pow},{Mod},{SetTem},{WdSpd},{Air},{Blo},{Health},{SwhSlp},{Lig},{SwingLfRig},{SwUpDn},{Quiet},{Tur},{StHt},{TemUn},{HeatCoolType},{TemRec},{SvSt},{SlpMod}],"t":"cmd"'.format(**self._acOptions) + '}'
sentJsonPayload = '{"cid":"app","i":0,"pack":"' + base64.b64encode(self.CIPHER.encrypt(self.Pad(statePackJson).encode("utf8"))).decode('utf-8') + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid":{}'.format(self._uid) + '}'
# Setup UDP Client & start transfering
clientSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
clientSock.settimeout(timeout)
clientSock.sendto(bytes(sentJsonPayload, "utf-8"), (self._ip_addr, self._port))
data, addr = clientSock.recvfrom(64000)
receivedJson = simplejson.loads(data)
clientSock.close()
pack = receivedJson['pack']
base64decodedPack = base64.b64decode(pack)
decryptedPack = self.CIPHER.decrypt(base64decodedPack)
decodedPack = decryptedPack.decode("utf-8")
replacedPack = decodedPack.replace('\x0f', '').replace(decodedPack[decodedPack.rindex('}')+1:], '')
receivedJsonPayload = simplejson.loads(replacedPack)
if self.encryption_version == 1:
cipher = self.CIPHER
sentJsonPayload = '{"cid":"app","i":0,"pack":"' + base64.b64encode(cipher.encrypt(self.Pad(statePackJson).encode("utf8"))).decode('utf-8') + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid":{}'.format(self._uid) + '}'
elif self.encryption_version == 2:
pack, tag = self.EncryptGCM(self._encryption_key, statePackJson)
sentJsonPayload = '{"cid":"app","i":0,"pack":"' + pack + '","t":"pack","tcid":"' + str(self._mac_addr) + '","uid":{}'.format(self._uid) + ',"tag":"' + tag +'"}'
cipher = self.GetGCMCipher(self._encryption_key)
receivedJsonPayload = self.FetchResult(cipher, self._ip_addr, self._port, timeout, sentJsonPayload)
_LOGGER.info('Done sending state to HVAC: ' + str(receivedJsonPayload))

def UpdateHATargetTemperature(self):
Expand Down

0 comments on commit ca39595

Please sign in to comment.