import nymea import time import threading import json import requests import random from socket import * import sys pollTimer = None pollFrequency = 30 ipMap = {} def discoverThings(info): logger.log("Discovery started for", info.thingClassId) discoveredIps = findIps() for i in range(0, len(discoveredIps)): deviceIp = discoveredIps[i] # get basic info /common/basic_info # response example: # ret=OK,type=aircon,reg=eu,dst=1,ver=1_14_58,rev=83B3526,pow=0,err=0,location=0, # name=name-in-%hex,icon=0,method=polling,port=30050,id=uuid, # pw=,lpw_flag=0,adp_kind=3,pv=3.30,cpv=3,cpv_minor=20,led=1,en_setzone=1,mac=mac,adp_mode=run,en_hol=0, # ssid1=ssid,radio1=-43,ssid=DaikinAP14937,grp_name=,en_grp=0 # get model info /aircon/get_model_info --> (how) can we use this? # response example: # ret=OK,model=0FC3,type=N,pv=3.30,cpv=3,cpv_minor=20,mid=NA,humd=0,s_humd=0,acled=0,land=0,elec=1,temp=1, # temp_rng=0,m_dtct=1,ac_dst=--,disp_dry=0,dmnd=1,en_scdltmr=1,en_frate=1,en_fdir=1,s_fdir=3,en_rtemp_a=0, # en_spmode=5,en_ipw_sep=1,en_mompow=1,hmlmt_l=10.0 aUrl = 'http://' + deviceIp + '/common/basic_info' headers = {'Accept': '*/*'} rr = requests.get(aUrl, headers=headers) pollResponse = rr.text if rr.status_code == requests.codes.ok: systemType = 'none' splitResponse = pollResponse.split(",") for j in range(0, len(splitResponse)): splitItem = splitResponse[j].split("=") if splitItem[0] == 'type': systemType = splitItem[1] elif splitItem[0] == 'id': systemId = splitItem[1] elif splitItem[0] == 'name': hexArray = splitItem[1].split("%") deviceName = "" for k in range(0, len(hexArray)): deviceName+=bytes.fromhex(hexArray[k]).decode('utf-8') if systemType == 'aircon': logger.log("Device with IP " + deviceIp + " is a Daikin airco unit.") logger.log("Device ID:", systemId) logger.log("Device Name:", deviceName) # check if device already known exists = False for thing in myThings(): logger.log("Comparing to existing units: is %s an airco unit?" % (thing.name)) if thing.thingClassId == aircoThingClassId: logger.log("Yes, %s is an airco unit." % (thing.name)) if thing.paramValue(aircoThingDeviceIdParamTypeId) == systemId: logger.log("Already have unit with serial number %s in the system: %s" % (systemId, thing.name)) exists = True else: logger.log("Thing %s doesn't match with found unit with serial number %s" % (thing.name, systemId)) if exists == False: # unit doesn't exist yet, so add it thingDescriptor = nymea.ThingDescriptor(aircoThingClassId, deviceName) thingDescriptor.params = [ nymea.Param(aircoThingDeviceIdParamTypeId, systemId), nymea.Param(aircoThingDeviceNameParamTypeId, deviceName) ] info.addDescriptor(thingDescriptor) else: # unit already exists, so show it to allow reconfiguration thingDescriptor = nymea.ThingDescriptor(aircoThingClassId, deviceName, thingId=thing.id) thingDescriptor.params = [ nymea.Param(aircoThingDeviceIdParamTypeId, systemId), nymea.Param(aircoThingDeviceNameParamTypeId, deviceName) ] info.addDescriptor(thingDescriptor) else: logger.log("Device with IP " + deviceIp + " does not appear to be a supported Daikin airco unit.") else: logger.log("Device with IP " + deviceIp + " does not appear to be a supported Daikin airco unit.") info.finish(nymea.ThingErrorNoError) return def findIps(): discoveredIps = [] # Create a UDP socket sock = socket(AF_INET, SOCK_DGRAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) sock.settimeout(20) server_address = ('255.255.255.255', 30050) message = 'DAIKIN_UDP/common/basic_info' try: while True: # Send data logger.log('sending: ' + message) sent = sock.sendto(message.encode(), server_address) # Receive response logger.log('waiting to receive') data, server = sock.recvfrom(4096) if data.decode('UTF-8')[0:18] == 'ret=OK,type=aircon': print('Received confirmation; server ip: ' + str(server[0]) ) discoveredIps.append(str(server[0])) break else: print('Verification failed') print('Trying again...') finally: sock.close() return discoveredIps def findDeviceIP(airco): searchSystemId = airco.paramValue(aircoThingDeviceIdParamTypeId) discoveredIps = findIps() foundIp = None found = False for i in range(0, len(discoveredIps)): deviceIp = discoveredIps[i] aUrl = 'http://' + deviceIp + '/common/basic_info' headers = {'Accept': '*/*'} rr = requests.get(aUrl, headers=headers) pollResponse = rr.text if rr.status_code == requests.codes.ok: logger.log("Device with IP " + deviceIp + " is a supported Daikin Airco.") # get device info splitResponse = pollResponse.split(",") for j in range(0, len(splitResponse)): splitItem = splitResponse[j].split("=") if splitItem[0] == 'id': systemId = splitItem[1] logger.log("Device ID:", systemId) # check if this is the device with the serial number we're looking for if systemId == searchSystemId: logger.log("Device with IP " + deviceIp + " is the existing device.") found = True foundIp = deviceIp ipMap[airco] = foundIp else: logger.log("Device with IP " + deviceIp + " does not appear to be a supported Daikin Airco.") if found == True: airco.setStateValue(aircoConnectedStateTypeId, True) else: airco.setStateValue(aircoConnectedStateTypeId, False) return found def setupThing(info): searchSystemId = info.thing.paramValue(aircoThingDeviceIdParamTypeId) logger.log("setupThing called for", info.thing.name, searchSystemId) found = findDeviceIP(info.thing) if found == True: pollAirco(info.thing) info.finish(nymea.ThingErrorNoError) else: info.finish(nymea.ThingErrorHardwareFailure, "Error connecting to the device in the network.") logger.log("Airco added:", info.thing.name) # If no poll timer is set up yet, start it now logger.log("Creating pollService") global pollTimer global pollFrequency if pollTimer == None: logger.log("Starting timer @ setupThing") pollTimer = nymea.PluginTimer(pollFrequency, pollService) logger.log("timer interval @ setupThing", pollTimer.interval) else: logger.log("Timer already exists @ setupThing") info.finish(nymea.ThingErrorNoError) return def pollAirco(info): global pollFrequency global ipMap deviceIp = ipMap[info] logger.log("polling airco", deviceIp, info.name) airco = info headers = {'Accept': '*/*'} # get sensor info # response example: # ret=OK,htemp=25.0,hhum=30,otemp=2.0,err=0,cmpfreq=0,mompow=1 aUrl = 'http://' + deviceIp + '/aircon/get_sensor_info' pr = requests.get(aUrl, headers=headers) pollResponse = pr.text if pr.status_code == requests.codes.ok: airco.setStateValue(aircoConnectedStateTypeId, True) splitResponse = pollResponse.split(",") for i in range(0, len(splitResponse)): splitItem = splitResponse[i].split("=") if splitItem[0] == 'htemp': homeTemp = float(splitItem[1]) airco.setStateValue(aircoTemperatureStateTypeId, homeTemp) elif splitItem[0] == 'hhum': homeHumidity = float(splitItem[1]) airco.setStateValue(aircoHumidityStateTypeId, homeHumidity) elif splitItem[0] == 'otemp': outTemp = float(splitItem[1]) airco.setStateValue(aircoOutsideTemperatureStateTypeId, outTemp) else: airco.setStateValue(aircoConnectedStateTypeId, False) #device disconnected, IP may have changed, so search again found = findDeviceIP(airco) if found == False: return # get control info # response example: # ret=OK,pow=0,mode=4,adv=,stemp=21.0,shum=0,dt1=22.0,dt2=M,dt3=20.0,dt4=21.0,dt5=21.0,dt7=22.0, # dh1=0,dh2=0,dh3=0,dh4=0,dh5=0,dh7=0,dhh=50,b_mode=4,b_stemp=21.0,b_shum=0,alert=255, # f_rate=A,f_dir=0,b_f_rate=A,b_f_dir=0,dfr1=A,dfr2=A,dfr3=A,dfr4=A,dfr5=A,dfr6=A,dfr7=A,dfrh=5, # dfd1=0,dfd2=0,dfd3=0,dfd4=0,dfd5=0,dfd6=0,dfd7=0,dfdh=0,dmnd_run=0,en_demand=1 aUrl = 'http://' + deviceIp + '/aircon/get_control_info' pr = requests.get(aUrl, headers=headers) pollResponse = pr.text if pr.status_code == requests.codes.ok: airco.setStateValue(aircoConnectedStateTypeId, True) splitResponse = pollResponse.split(",") for i in range(0, len(splitResponse)): splitItem = splitResponse[i].split("=") # get power state if splitItem[0] == 'pow': power = int(splitItem[1]) logger.log("Power", power) airco.setStateValue(aircoPowerStateTypeId, power) elif splitItem[0] == 'mode': mode = int(splitItem[1]) if mode == 2: # 2 = dehumidifier mode logger.log("Airco mode: dehumidifier", mode) airco.setStateValue(aircoModeStateTypeId, "Dehumidifier") elif mode == 3: # 3 = cooling mode logger.log("Airco mode: cooling", mode) airco.setStateValue(aircoModeStateTypeId, "Cooling") elif mode == 4: # 4 = heating mode logger.log("Airco mode: heating", mode) airco.setStateValue(aircoModeStateTypeId, "Heating") elif mode == 6: # 6 = fan mode logger.log("Airco mode: fan", mode) airco.setStateValue(aircoModeStateTypeId, "Fan") else: # 0-1-7 = automatic mode logger.log("Airco mode: automatic", mode) airco.setStateValue(aircoModeStateTypeId, "Automatic") elif splitItem[0] == 'stemp': try: targetTemp = float(splitItem[1]) logger.log("Target temperature", targetTemp) airco.setStateValue(aircoTargetTemperatureStateTypeId, targetTemp) except: logger.log("Target temperature parameter is not a number:", splitItem[1]) for j in range(0, len(splitResponse)): splitItem = splitResponse[j].split("=") if splitItem[0] == 'dt1': targetTemp = float(splitItem[1]) logger.log("Using target temperature from automatic mode", targetTemp) airco.setStateValue(aircoTargetTemperatureStateTypeId, targetTemp) else: airco.setStateValue(aircoConnectedStateTypeId, False) #set heatingOn / coolingOn based on mode, temperature & target temperature (API doesn't seem to return it) targetTemperature = airco.stateValue(aircoTargetTemperatureStateTypeId) temperature = airco.stateValue(aircoTemperatureStateTypeId) mode = airco.stateValue(aircoModeStateTypeId) power = airco.stateValue(aircoPowerStateTypeId) if mode == "Cooling" and power == True: airco.setStateValue(aircoHeatingOnStateTypeId, False) if targetTemperature < temperature: airco.setStateValue(aircoCoolingOnStateTypeId, True) else: airco.setStateValue(aircoCoolingOnStateTypeId, False) elif mode == "Heating" and power == True: airco.setStateValue(aircoCoolingOnStateTypeId, False) if targetTemperature > temperature: airco.setStateValue(aircoHeatingOnStateTypeId, True) else: airco.setStateValue(aircoHeatingOnStateTypeId, False) elif mode == "Automatic" and power == True: if targetTemperature == temperature: airco.setStateValue(aircoCoolingOnStateTypeId, False) airco.setStateValue(aircoHeatingOnStateTypeId, False) elif targetTemperature > temperature: airco.setStateValue(aircoCoolingOnStateTypeId, False) airco.setStateValue(aircoHeatingOnStateTypeId, True) else: airco.setStateValue(aircoCoolingOnStateTypeId, True) airco.setStateValue(aircoHeatingOnStateTypeId, False) else: airco.setStateValue(aircoCoolingOnStateTypeId, False) airco.setStateValue(aircoHeatingOnStateTypeId, False) def pollService(): logger.log("pollTimer triggered") global pollTimer global pollFrequency pollTimer.interval = pollFrequency # Poll all airco units we know for thing in myThings(): if thing.thingClassId == aircoThingClassId: pollAirco(thing) def executeAction(info): global ipMap deviceIp = ipMap[info.thing] logger.log("executeAction called for thing", info.thing.name, deviceIp, info.actionTypeId, info.params) headers = {'Accept': '*/*'} aUrl = 'http://' + deviceIp + '/aircon/get_control_info' pr = requests.get(aUrl, headers=headers) # get control info, to obtain target temperature per mode, to set the correct target temperature (stemp) when changing mode pollResponse = pr.text logger.log("response", pr.text) if pr.status_code == requests.codes.ok: info.thing.setStateValue(aircoConnectedStateTypeId, True) splitResponse = pollResponse.split(",") for i in range(0, len(splitResponse)): splitItem = splitResponse[i].split("=") if splitItem[0] == 'pow': power = int(splitItem[1]) elif splitItem[0] == 'mode': mode = int(splitItem[1]) elif splitItem[0] == 'stemp': targetTemp = splitItem[1] elif splitItem[0] == 'shum': targetHum = splitItem[1] elif splitItem[0] == 'f_rate': f_rate = splitItem[1] elif splitItem[0] == 'f_dir': f_dir = splitItem[1] elif splitItem[0] == 'dt1': dt1 = splitItem[1] elif splitItem[0] == 'dt2': dt2 = splitItem[1] elif splitItem[0] == 'dt3': dt3 = splitItem[1] elif splitItem[0] == 'dt4': dt4 = splitItem[1] elif splitItem[0] == 'dt5': dt5 = splitItem[1] elif splitItem[0] == 'dt7': dt7 = splitItem[1] elif splitItem[0] == 'dh1': dh1 = splitItem[1] elif splitItem[0] == 'dh2': dh2 = splitItem[1] elif splitItem[0] == 'dh3': dh3 = splitItem[1] elif splitItem[0] == 'dh4': dh4 = splitItem[1] elif splitItem[0] == 'dh5': dh5 = splitItem[1] elif splitItem[0] == 'dh7': dh7 = splitItem[1] else: info.thing.setStateValue(aircoConnectedStateTypeId, False) info.finish(nymea.ThingErrorNoError) if info.actionTypeId == aircoPowerActionTypeId: power = info.paramValue(aircoPowerActionPowerParamTypeId) if power == True: power = 1 elif power == False: power = 0 logger.log("new power value", power) elif info.actionTypeId == aircoTargetTemperatureActionTypeId: targetTemp = str(info.paramValue(aircoTargetTemperatureActionTargetTemperatureParamTypeId)) logger.log("new target temperature", targetTemp) elif info.actionTypeId == aircoModeActionTypeId: targetMode = info.paramValue(aircoModeActionModeParamTypeId) if targetMode == "Automatic": mode = 0 targetTemp = dt1 targetHum = dh1 elif targetMode == "Cooling": mode = 3 targetTemp = dt3 targetHum = dh3 elif targetMode == "Heating": mode = 4 targetTemp = dt4 targetHum = dh4 elif targetMode == "Dehumidifier": mode = 2 targetTemp = dt2 targetHum = dh2 elif targetMode == "Fan": mode = 6 logger.log("new target mode:", targetMode, mode) # mandatory parameters: pow, mode, stemp, shum, f_rate, f_dir (f_rate & f_dir can't be set from the plugin yet, so we always use the values obtained above) param = "pow="+str(power)+"&mode="+str(mode)+"&stemp="+targetTemp+"&shum="+targetHum+"&f_rate="+f_rate+"&f_dir="+f_dir logger.log("parameters", param) aUrl = "http://" + deviceIp + "/aircon/set_control_info?"+param pr = requests.post(aUrl, headers=headers) time.sleep(0.5) pollAirco(info.thing) info.finish(nymea.ThingErrorNoError) return def deinit(): global pollTimer # If we started a poll timer, cancel it on shutdown. if pollTimer is not None: pollTimer = None def thingRemoved(thing): global pollTimer logger.log("removeThing called for", thing.name) # Clean up all data related to this thing if len(myThings()) == 0 and pollTimer is not None: pollTimer = None