├── README.md └── lifesmart ├── __init__.py ├── binary_sensor.py ├── climate.py ├── cover.py ├── light.py ├── manifest.json ├── sensor.py ├── services.yaml └── switch.py /README.md: -------------------------------------------------------------------------------- 1 | 使用说明 2 | ==== 3 | lifesmart 设备接入 HomeAssistant插件 4 | 5 | 更新说明 6 | ------- 7 | [latest] 8 | 9 | 1、基于新版本homeassistant更新依赖名,解决新版本失效的问题; 10 | 11 | 2、变更lifesmart认证方式为账号密码。 12 | 13 | [2020年12月26日更新] 14 | 15 | 支持流光开关灯光控制 16 | 17 | 更新manifest内容以适配新版本home assistant 18 | 19 | [2020年8月21日更新] 20 | 21 | 新增设备支持: 22 | 23 | **超能面板**:SL_NATURE 24 | 25 | PS:其实就是个开关... 26 | 27 | [2020年2月4日更新] 28 | 29 | 优化实体ID生成逻辑:解决未加入或存在多个智慧中心时,me号可能存在重复的问题。 30 | 31 | [2019年12月6日更新] 32 | 33 | 新增支持设备: 34 | 35 | **中央空调面板**:V_AIR_P 36 | 37 | **智能门锁反馈信息**:SL_LK_LS、SL_LK_GTM、SL_LK_AG、SL_LK_SG、SL_LK_YL 38 | 39 | 目前支持的设备: 40 | ------- 41 | 1、开关; 42 | 43 | 2、灯光:目前仅支持超级碗夜灯; 44 | 45 | 3、万能遥控; 46 | 47 | 4、窗帘电机(仅支持杜亚电机) 48 | 49 | 5、动态感应器、门磁、环境感应器、甲醛/燃气感应器 50 | 51 | 6、空调控制面板 52 | 53 | 7、智能门锁信息反馈 54 | 55 | 使用方法: 56 | ------- 57 | 1、将lifesmart目录复制到config/custom_components/下 58 | 59 | 2、在configuration.yaml文件中增加配置: 60 | 61 | ``` 62 | lifesmart: 63 | appkey: "your_appkey" 64 | apptoken: "your_apptoken" 65 | username: "your_username" 66 | password: "your_password" 67 | exclude: 68 | - "0011" #需屏蔽设备的me值,这个暂时为必填项,可以填任意内容 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /lifesmart/__init__.py: -------------------------------------------------------------------------------- 1 | """lifesmart by @skyzhishui""" 2 | import subprocess 3 | import urllib.request 4 | import json 5 | import time 6 | import datetime 7 | import hashlib 8 | import logging 9 | import threading 10 | import websocket 11 | import asyncio 12 | 13 | import voluptuous as vol 14 | import sys 15 | sys.setrecursionlimit(100000) 16 | 17 | from homeassistant.const import ( 18 | CONF_FRIENDLY_NAME, 19 | ) 20 | from homeassistant.components.climate.const import ( 21 | HVAC_MODE_AUTO, 22 | HVAC_MODE_COOL, 23 | HVAC_MODE_FAN_ONLY, 24 | HVAC_MODE_HEAT, 25 | HVAC_MODE_DRY, 26 | SUPPORT_FAN_MODE, 27 | SUPPORT_TARGET_TEMPERATURE, 28 | HVAC_MODE_OFF, 29 | ) 30 | from homeassistant.core import callback 31 | from homeassistant.helpers import discovery 32 | import homeassistant.helpers.config_validation as cv 33 | from homeassistant.helpers.entity import Entity 34 | from homeassistant.helpers.event import async_track_point_in_utc_time 35 | from homeassistant.util.dt import utcnow 36 | 37 | _LOGGER = logging.getLogger(__name__) 38 | 39 | CONF_LIFESMART_APPKEY = "appkey" 40 | CONF_LIFESMART_APPTOKEN = "apptoken" 41 | CONF_LIFESMART_USERTOKEN = "usertoken" 42 | CONF_LIFESMART_USERNAME = "username" 43 | CONF_LIFESMART_PASSWORD = "password" 44 | CONF_LIFESMART_USERID = "userid" 45 | CONF_EXCLUDE_ITEMS = "exclude" 46 | SWTICH_TYPES = ["SL_SF_RC", 47 | "SL_SW_RC", 48 | "SL_SW_IF3", 49 | "SL_SF_IF3", 50 | "SL_SW_CP3", 51 | "SL_SW_RC3", 52 | "SL_SW_IF2", 53 | "SL_SF_IF2", 54 | "SL_SW_CP2", 55 | "SL_SW_FE2", 56 | "SL_SW_RC2", 57 | "SL_SW_ND2", 58 | "SL_MC_ND2", 59 | "SL_SW_IF1", 60 | "SL_SF_IF1", 61 | "SL_SW_CP1", 62 | "SL_SW_FE1", 63 | "SL_OL_W", 64 | "SL_SW_RC1", 65 | "SL_SW_ND1", 66 | "SL_MC_ND1", 67 | "SL_SW_ND3", 68 | "SL_MC_ND3", 69 | "SL_SW_ND2", 70 | "SL_MC_ND2", 71 | "SL_SW_ND1", 72 | "SL_MC_ND1", 73 | "SL_S", 74 | "SL_SPWM", 75 | "SL_P_SW", 76 | "SL_SW_DM1", 77 | "SL_SW_MJ2", 78 | "SL_SW_MJ1", 79 | "SL_OL", 80 | "SL_OL_3C", 81 | "SL_OL_DE", 82 | "SL_OL_UK", 83 | "SL_OL_UL", 84 | "OD_WE_OT1", 85 | "SL_NATURE" 86 | ] 87 | LIGHT_SWITCH_TYPES = ["SL_OL_W", 88 | "SL_SW_IF1", 89 | "SL_SW_IF2", 90 | "SL_SW_IF3", 91 | ] 92 | QUANTUM_TYPES=["OD_WE_QUAN", 93 | ] 94 | 95 | SPOT_TYPES = ["MSL_IRCTL", 96 | "OD_WE_IRCTL", 97 | "SL_SPOT"] 98 | BINARY_SENSOR_TYPES = ["SL_SC_G", 99 | "SL_SC_BG", 100 | "SL_SC_MHW ", 101 | "SL_SC_BM", 102 | "SL_SC_CM", 103 | "SL_P_A"] 104 | COVER_TYPES = ["SL_DOOYA"] 105 | GAS_SENSOR_TYPES = ["SL_SC_WA ", 106 | "SL_SC_CH", 107 | "SL_SC_CP", 108 | "ELIQ_EM"] 109 | EV_SENSOR_TYPES = ["SL_SC_THL", 110 | "SL_SC_BE", 111 | "SL_SC_CQ"] 112 | OT_SENSOR_TYPES = ["SL_SC_MHW", 113 | "SL_SC_BM", 114 | "SL_SC_G", 115 | "SL_SC_BG"] 116 | LOCK_TYPES = ["SL_LK_LS", 117 | "SL_LK_GTM", 118 | "SL_LK_AG", 119 | "SL_LK_SG", 120 | "SL_LK_YL"] 121 | 122 | SPEED_OFF = "Speed_Off" 123 | SPEED_LOW = "Speed_Low" 124 | SPEED_MEDIUM = "Speed_Medium" 125 | SPEED_HIGH = "Speed_High" 126 | 127 | LIFESMART_STATE_LIST = [HVAC_MODE_OFF, 128 | HVAC_MODE_AUTO, 129 | HVAC_MODE_FAN_ONLY, 130 | HVAC_MODE_COOL, 131 | HVAC_MODE_HEAT, 132 | HVAC_MODE_DRY] 133 | 134 | CLIMATE_TYPES = ["V_AIR_P", 135 | "SL_CP_DN"] 136 | 137 | ENTITYID = 'entity_id' 138 | DOMAIN = 'lifesmart' 139 | 140 | LifeSmart_STATE_MANAGER = 'lifesmart_wss' 141 | 142 | 143 | def lifesmart_EpGetAll(appkey,apptoken,usertoken,userid): 144 | url = "https://api.ilifesmart.com/app/api.EpGetAll" 145 | tick = int(time.time()) 146 | sdata = "method:EpGetAll,time:"+str(tick)+",userid:"+userid+",usertoken:"+usertoken+",appkey:"+appkey+",apptoken:"+apptoken 147 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 148 | send_values ={ 149 | "id": 1, 150 | "method": "EpGetAll", 151 | "system": { 152 | "ver": "1.0", 153 | "lang": "en", 154 | "userid": userid, 155 | "appkey": appkey, 156 | "time": tick, 157 | "sign": sign 158 | } 159 | } 160 | header = {'Content-Type': 'application/json'} 161 | send_data = json.dumps(send_values) 162 | req = urllib.request.Request(url=url, data=send_data.encode('utf-8'), headers=header, method='POST') 163 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 164 | if response['code'] == 0: 165 | return response['message'] 166 | return False 167 | 168 | 169 | def lifesmart_Sendkeys(appkey,apptoken,usertoken,userid,agt,ai,me,category,brand,keys): 170 | url = "https://api.ilifesmart.com/app/irapi.SendKeys" 171 | tick = int(time.time()) 172 | #keys = str(keys) 173 | sdata = "method:SendKeys,agt:"+agt+",ai:"+ai+",brand:"+brand+",category:"+category+",keys:"+keys+",me:"+me+",time:"+str(tick)+",userid:"+userid+",usertoken:"+usertoken+",appkey:"+appkey+",apptoken:"+apptoken 174 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 175 | _LOGGER.debug("sendkey: %s",str(sdata)) 176 | send_values ={ 177 | "id": 1, 178 | "method": "SendKeys", 179 | "params": { 180 | "agt": agt, 181 | "me": me, 182 | "category": category, 183 | "brand": brand, 184 | "ai": ai, 185 | "keys": keys 186 | }, 187 | "system": { 188 | "ver": "1.0", 189 | "lang": "en", 190 | "userid": userid, 191 | "appkey": appkey, 192 | "time": tick, 193 | "sign": sign 194 | } 195 | } 196 | header = {'Content-Type': 'application/json'} 197 | send_data = json.dumps(send_values) 198 | req = urllib.request.Request(url=url, data=send_data.encode('utf-8'), headers=header, method='POST') 199 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 200 | _LOGGER.debug("sendkey_res: %s",str(response)) 201 | return response 202 | def lifesmart_Sendackeys(appkey,apptoken,usertoken,userid,agt,ai,me,category,brand,keys,power,mode,temp,wind,swing): 203 | url = "https://api.ilifesmart.com/app/irapi.SendACKeys" 204 | tick = int(time.time()) 205 | #keys = str(keys) 206 | sdata = "method:SendACKeys,agt:"+agt+",ai:"+ai+",brand:"+brand+",category:"+category+",keys:"+keys+",me:"+me+",mode:"+str(mode)+",power:"+str(power)+",swing:"+str(swing)+",temp:"+str(temp)+",wind:"+str(wind)+",time:"+str(tick)+",userid:"+userid+",usertoken:"+usertoken+",appkey:"+appkey+",apptoken:"+apptoken 207 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 208 | _LOGGER.debug("sendackey: %s",str(sdata)) 209 | send_values ={ 210 | "id": 1, 211 | "method": "SendACKeys", 212 | "params": { 213 | "agt": agt, 214 | "me": me, 215 | "category": category, 216 | "brand": brand, 217 | "ai": ai, 218 | "keys": keys, 219 | "power": power, 220 | "mode": mode, 221 | "temp": temp, 222 | "wind": wind, 223 | "swing": swing 224 | }, 225 | "system": { 226 | "ver": "1.0", 227 | "lang": "en", 228 | "userid": userid, 229 | "appkey": appkey, 230 | "time": tick, 231 | "sign": sign 232 | } 233 | } 234 | header = {'Content-Type': 'application/json'} 235 | send_data = json.dumps(send_values) 236 | req = urllib.request.Request(url=url, data=send_data.encode('utf-8'), headers=header, method='POST') 237 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 238 | _LOGGER.debug("sendackey_res: %s",str(response)) 239 | return response 240 | 241 | def lifesmart_Login(uid,pwd,appkey): 242 | url = "https://api.ilifesmart.com/app/auth.login" 243 | login_data = { 244 | "uid": uid, 245 | "pwd": pwd, 246 | "appkey": appkey 247 | } 248 | header = {'Content-Type': 'application/json'} 249 | req = urllib.request.Request(url=url, data=json.dumps(login_data).encode('utf-8'),headers=header, method='POST') 250 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 251 | return response 252 | 253 | def lifesmart_doAuth(userid,token,appkey): 254 | url = "https://api.ilifesmart.com/app/auth.do_auth" 255 | auth_data = { 256 | "userid": userid, 257 | "token": token, 258 | "appkey": appkey, 259 | "rgn": "cn" 260 | } 261 | header = {'Content-Type': 'application/json'} 262 | req = urllib.request.Request(url=url, data=json.dumps(auth_data).encode('utf-8'),headers=header, method='POST') 263 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 264 | return response 265 | 266 | def setup(hass, config): 267 | """Set up the lifesmart component.""" 268 | param = {} 269 | param['appkey'] = config[DOMAIN][CONF_LIFESMART_APPKEY] 270 | param['apptoken'] = config[DOMAIN][CONF_LIFESMART_APPTOKEN] 271 | #param['usertoken'] = config[DOMAIN][CONF_LIFESMART_USERTOKEN] 272 | #param['userid'] = config[DOMAIN][CONF_LIFESMART_USERID] 273 | param['username'] = config[DOMAIN][CONF_LIFESMART_USERNAME] 274 | param['passwd'] = config[DOMAIN][CONF_LIFESMART_PASSWORD] 275 | res_login = lifesmart_Login(param['username'],param['passwd'],param['appkey']) 276 | if res_login['code'] == "error": 277 | _LOGGER.error("login fail: %s",str(res_login['message'])) 278 | param['token'] = res_login['token'] 279 | param['userid'] = res_login['userid'] 280 | res_doauth = lifesmart_doAuth(param['userid'],param['token'],param['appkey']) 281 | if res_doauth['code'] == "error": 282 | _LOGGER.error("login fail: %s",str(res_doauth['message'])) 283 | param['usertoken'] = res_doauth['usertoken'] 284 | exclude_items = config[DOMAIN][CONF_EXCLUDE_ITEMS] 285 | devices = lifesmart_EpGetAll(param['appkey'],param['apptoken'],param['usertoken'],param['userid']) 286 | for dev in devices: 287 | if dev['me'] in exclude_items: 288 | continue 289 | devtype = dev['devtype'] 290 | dev['agt'] = dev['agt'].replace("_","") 291 | if devtype in SWTICH_TYPES: 292 | discovery.load_platform(hass,"switch", DOMAIN, {"dev": dev,"param": param}, config) 293 | elif devtype in BINARY_SENSOR_TYPES: 294 | discovery.load_platform(hass,"binary_sensor", DOMAIN, {"dev": dev,"param": param}, config) 295 | elif devtype in COVER_TYPES: 296 | discovery.load_platform(hass,"cover", DOMAIN, {"dev": dev,"param": param}, config) 297 | elif devtype in SPOT_TYPES: 298 | discovery.load_platform(hass,"light", DOMAIN, {"dev": dev,"param": param}, config) 299 | elif devtype in CLIMATE_TYPES: 300 | discovery.load_platform(hass,"climate", DOMAIN, {"dev": dev,"param": param}, config) 301 | elif devtype in GAS_SENSOR_TYPES or devtype in EV_SENSOR_TYPES: 302 | discovery.load_platform(hass,"sensor", DOMAIN, {"dev": dev,"param": param}, config) 303 | if devtype in OT_SENSOR_TYPES: 304 | discovery.load_platform(hass,"sensor", DOMAIN, {"dev": dev,"param": param}, config) 305 | if devtype in LIGHT_SWITCH_TYPES: 306 | discovery.load_platform(hass,"light", DOMAIN, {"dev": dev,"param": param}, config) 307 | 308 | def send_keys(call): 309 | """Handle the service call.""" 310 | agt = call.data['agt'] 311 | me = call.data['me'] 312 | ai = call.data['ai'] 313 | category = call.data['category'] 314 | brand = call.data['brand'] 315 | keys = call.data['keys'] 316 | restkey = lifesmart_Sendkeys(param['appkey'],param['apptoken'],param['usertoken'],param['userid'],agt,ai,me,category,brand,keys) 317 | _LOGGER.debug("sendkey: %s",str(restkey)) 318 | def send_ackeys(call): 319 | """Handle the service call.""" 320 | agt = call.data['agt'] 321 | me = call.data['me'] 322 | ai = call.data['ai'] 323 | category = call.data['category'] 324 | brand = call.data['brand'] 325 | keys = call.data['keys'] 326 | power = call.data['power'] 327 | mode = call.data['mode'] 328 | temp = call.data['temp'] 329 | wind = call.data['wind'] 330 | swing = call.data['swing'] 331 | restackey = lifesmart_Sendackeys(param['appkey'],param['apptoken'],param['usertoken'],param['userid'],agt,ai,me,category,brand,keys,power,mode,temp,wind,swing) 332 | _LOGGER.debug("sendkey: %s",str(restackey)) 333 | 334 | def get_fan_mode(_fanspeed): 335 | fanmode = None 336 | if _fanspeed < 30: 337 | fanmode = SPEED_LOW 338 | elif _fanspeed < 65 and _fanspeed >= 30: 339 | fanmode = SPEED_MEDIUM 340 | elif _fanspeed >=65: 341 | fanmode = SPEED_HIGH 342 | return fanmode 343 | 344 | async def set_Event(msg): 345 | if msg['msg']['idx'] != "s" and msg['msg']['me'] not in exclude_items: 346 | devtype = msg['msg']['devtype'] 347 | agt = msg['msg']['agt'].replace("_","") 348 | if devtype in SWTICH_TYPES and msg['msg']['idx'] in ["L1","L2","L3","P1","P2","P3"]: 349 | enid = "switch."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 350 | attrs = hass.states.get(enid).attributes 351 | if msg['msg']['type'] % 2 == 1: 352 | hass.states.set(enid, 'on',attrs) 353 | else: 354 | hass.states.set(enid, 'off',attrs) 355 | elif devtype in BINARY_SENSOR_TYPES and msg['msg']['idx'] in ["M","G","B","AXS","P1"]: 356 | enid = "binary_sensor."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 357 | attrs = hass.states.get(enid).attributes 358 | if msg['msg']['val'] == 1: 359 | hass.states.set(enid, 'on',attrs) 360 | else: 361 | hass.states.set(enid, 'off',attrs) 362 | elif devtype in COVER_TYPES and msg['msg']['idx'] == "P1": 363 | enid = "cover."+(devtype + "_" + agt + "_" + msg['msg']['me']).lower() 364 | attrs = dict(hass.states.get(enid).attributes) 365 | nval = msg['msg']['val'] 366 | ntype = msg['msg']['type'] 367 | attrs['current_position'] = nval & 0x7F 368 | _LOGGER.debug("websocket_cover_attrs: %s",str(attrs)) 369 | nstat = None 370 | if ntype % 2 == 0: 371 | if nval > 0: 372 | nstat = "open" 373 | else: 374 | nstat = "closed" 375 | else: 376 | if nval & 0x80 == 0x80: 377 | nstat = "opening" 378 | else: 379 | nstat = "closing" 380 | hass.states.set(enid, nstat, attrs) 381 | elif devtype in EV_SENSOR_TYPES: 382 | enid = "sensor."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 383 | attrs = hass.states.get(enid).attributes 384 | hass.states.set(enid, msg['msg']['v'], attrs) 385 | elif devtype in GAS_SENSOR_TYPES and msg['msg']['val'] > 0: 386 | enid = "sensor."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 387 | attrs = hass.states.get(enid).attributes 388 | hass.states.set(enid, msg['msg']['val'], attrs) 389 | elif devtype in SPOT_TYPES or devtype in LIGHT_SWITCH_TYPES: 390 | enid = "light."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 391 | attrs = hass.states.get(enid).attributes 392 | if msg['msg']['type'] % 2 == 1: 393 | hass.states.set(enid, 'on',attrs) 394 | else: 395 | hass.states.set(enid, 'off',attrs) 396 | #elif devtype in QUANTUM_TYPES and msg['msg']['idx'] == "P1": 397 | # enid = "light."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_P1").lower() 398 | # attrs = hass.states.get(enid).attributes 399 | # hass.states.set(enid, msg['msg']['val'], attrs) 400 | elif devtype in CLIMATE_TYPES: 401 | enid = "climate."+(devtype + "_" + agt + "_" + msg['msg']['me']).lower().replace(":","_").replace("@","_") 402 | #climate.v_air_p_a3yaaabbaegdrzcznti3mg_8ae5_1_2_1 403 | _idx = msg['msg']['idx'] 404 | attrs = dict(hass.states.get(enid).attributes) 405 | nstat = hass.states.get(enid).state 406 | _LOGGER.info("enid: %s",str(enid)) 407 | _LOGGER.info("_idx: %s",str(_idx)) 408 | _LOGGER.info("attrs: %s",str(attrs)) 409 | _LOGGER.info("nstat: %s",str(nstat)) 410 | if _idx == "O": 411 | if msg['msg']['type'] % 2 == 1: 412 | nstat = attrs['last_mode'] 413 | hass.states.set(enid, nstat, attrs) 414 | else: 415 | nstat = HVAC_MODE_OFF 416 | hass.states.set(enid, nstat, attrs) 417 | if _idx == "P1": 418 | if msg['msg']['type'] % 2 == 1: 419 | nstat = HVAC_MODE_HEAT 420 | hass.states.set(enid, nstat, attrs) 421 | else: 422 | nstat = HVAC_MODE_OFF 423 | hass.states.set(enid, nstat, attrs) 424 | if _idx == "P2": 425 | if msg['msg']['type'] % 2 == 1: 426 | attrs['Heating'] = "true" 427 | hass.states.set(enid, nstat, attrs) 428 | else: 429 | attrs['Heating'] = "false" 430 | hass.states.set(enid, nstat, attrs) 431 | elif _idx == "MODE": 432 | if msg['msg']['type'] == 206: 433 | if nstat != HVAC_MODE_OFF: 434 | nstat = LIFESMART_STATE_LIST[msg['msg']['val']] 435 | attrs['last_mode'] = LIFESMART_STATE_LIST[msg['msg']['val']] 436 | hass.states.set(enid, nstat, attrs) 437 | elif _idx == "F": 438 | if msg['msg']['type'] == 206: 439 | attrs['fan_mode'] = get_fan_mode(msg['msg']['val']) 440 | hass.states.set(enid, nstat, attrs) 441 | elif _idx == "tT" or _idx == "P3": 442 | if msg['msg']['type'] == 136: 443 | attrs['temperature'] = msg['msg']['v'] 444 | hass.states.set(enid, nstat, attrs) 445 | elif _idx == "T" or _idx == "P4": 446 | if msg['msg']['type'] == 8 or msg['msg']['type'] == 9: 447 | attrs['current_temperature'] = msg['msg']['v'] 448 | hass.states.set(enid, nstat, attrs) 449 | elif devtype in LOCK_TYPES: 450 | if msg['msg']['idx'] == "BAT": 451 | enid = "sensor."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 452 | attrs = hass.states.get(enid).attributes 453 | hass.states.set(enid, msg['msg']['val'], attrs) 454 | elif msg['msg']['idx'] == "EVTLO": 455 | enid = "binary_sensor."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 456 | val = msg['msg']['val'] 457 | ulk_way = val >> 12 458 | ulk_user = val & 0xfff 459 | ulk_success = True 460 | if ulk_user == 0: 461 | ulk_success = False 462 | attrs = {"unlocking_way": ulk_way,"unlocking_user": ulk_user,"devtype": devtype,"unlocking_success": ulk_success,"last_time": datetime.datetime.fromtimestamp(msg['msg']['ts']/1000).strftime("%Y-%m-%d %H:%M:%S") } 463 | if msg['msg']['type'] % 2 == 1: 464 | hass.states.set(enid, 'on',attrs) 465 | else: 466 | hass.states.set(enid, 'off',attrs) 467 | if devtype in OT_SENSOR_TYPES and msg['msg']['idx'] in ["Z","V","P3","P4"]: 468 | enid = "sensor."+(devtype + "_" + agt + "_" + msg['msg']['me'] + "_" + msg['msg']['idx']).lower() 469 | attrs = hass.states.get(enid).attributes 470 | hass.states.set(enid, msg['msg']['v'], attrs) 471 | def on_message(ws, message): 472 | _LOGGER.info("websocket_msg: %s",str(message)) 473 | msg = json.loads(message) 474 | if 'type' not in msg: 475 | return 476 | if msg['type'] != "io": 477 | return 478 | asyncio.run(set_Event(msg)) 479 | 480 | def on_error(ws, error): 481 | _LOGGER.debug("websocket_error: %s",str(error)) 482 | 483 | def on_close(ws): 484 | _LOGGER.debug("lifesmart websocket closed...") 485 | 486 | def on_open(ws): 487 | tick = int(time.time()) 488 | sdata = "method:WbAuth,time:"+str(tick)+",userid:"+param['userid']+",usertoken:"+param['usertoken']+",appkey:"+param['appkey']+",apptoken:"+param['apptoken'] 489 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 490 | send_values ={ 491 | "id": 1, 492 | "method": "WbAuth", 493 | "system": { 494 | "ver": "1.0", 495 | "lang": "en", 496 | "userid": param['userid'], 497 | "appkey": param['appkey'], 498 | "time": tick, 499 | "sign": sign 500 | } 501 | } 502 | header = {'Content-Type': 'application/json'} 503 | send_data = json.dumps(send_values) 504 | ws.send(send_data) 505 | _LOGGER.debug("lifesmart websocket sending_data...") 506 | 507 | hass.services.register(DOMAIN, 'send_keys', send_keys) 508 | hass.services.register(DOMAIN, 'send_ackeys', send_ackeys) 509 | ws = websocket.WebSocketApp("wss://api.ilifesmart.com:8443/wsapp/", 510 | on_message = on_message, 511 | on_error = on_error, 512 | on_close = on_close) 513 | ws.on_open = on_open 514 | hass.data[LifeSmart_STATE_MANAGER] = LifeSmartStatesManager(ws = ws) 515 | hass.data[LifeSmart_STATE_MANAGER].start_keep_alive() 516 | return True 517 | 518 | class LifeSmartEntity(Entity): 519 | """LifeSmart base device.""" 520 | 521 | def __init__(self, dev, idx, val, param): 522 | """Initialize the switch.""" 523 | self._name = dev['name'] + "_" + idx 524 | self._appkey = param['appkey'] 525 | self._apptoken = param['apptoken'] 526 | self._usertoken = param['usertoken'] 527 | self._userid = param['userid'] 528 | self._agt = dev['agt'] 529 | self._me = dev['me'] 530 | self._idx = idx 531 | self._devtype = dev['devtype'] 532 | attrs = {"agt": self._agt,"me": self._me,"idx": self._idx,"devtype": self._devtype } 533 | self._attributes = attrs 534 | 535 | 536 | @property 537 | def object_id(self): 538 | """Return LifeSmart device id.""" 539 | return self.entity_id 540 | 541 | @property 542 | def state_attrs(self): 543 | """Return the state attributes.""" 544 | return self._attributes 545 | 546 | @property 547 | def extra_state_attributes(self): 548 | """Return the extra state attributes of the device.""" 549 | return self._attributes 550 | 551 | @property 552 | def name(self): 553 | """Return LifeSmart device name.""" 554 | return self._name 555 | 556 | @property 557 | def assumed_state(self): 558 | """Return true if we do optimistic updates.""" 559 | return False 560 | 561 | @property 562 | def should_poll(self): 563 | """check with the entity for an updated state.""" 564 | return False 565 | 566 | 567 | @staticmethod 568 | def _lifesmart_epset(self, type, val, idx): 569 | #self._tick = int(time.time()) 570 | url = "https://api.ilifesmart.com/app/api.EpSet" 571 | tick = int(time.time()) 572 | appkey = self._appkey 573 | apptoken = self._apptoken 574 | userid = self._userid 575 | usertoken = self._usertoken 576 | agt = self._agt 577 | me = self._me 578 | sdata = "method:EpSet,agt:"+ agt +",idx:"+idx+",me:"+me+",type:"+type+",val:"+str(val)+",time:"+str(tick)+",userid:"+userid+",usertoken:"+usertoken+",appkey:"+appkey+",apptoken:"+apptoken 579 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 580 | send_values = { 581 | "id": 1, 582 | "method": "EpSet", 583 | "system": { 584 | "ver": "1.0", 585 | "lang": "en", 586 | "userid": userid, 587 | "appkey": appkey, 588 | "time": tick, 589 | "sign": sign 590 | }, 591 | "params": { 592 | "agt": agt, 593 | "me": me, 594 | "idx": idx, 595 | "type": type, 596 | "val": val 597 | } 598 | } 599 | header = {'Content-Type': 'application/json'} 600 | send_data = json.dumps(send_values) 601 | req = urllib.request.Request(url=url, data=send_data.encode('utf-8'), headers=header, method='POST') 602 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 603 | _LOGGER.info("epset_send: %s",str(send_data)) 604 | _LOGGER.info("epset_res: %s",str(response)) 605 | return response['code'] 606 | 607 | @staticmethod 608 | def _lifesmart_epget(self): 609 | url = "https://api.ilifesmart.com/app/api.EpGet" 610 | tick = int(time.time()) 611 | appkey = self._appkey 612 | apptoken = self._apptoken 613 | userid = self._userid 614 | usertoken = self._usertoken 615 | agt = self._agt 616 | me = self._me 617 | sdata = "method:EpGet,agt:"+ agt +",me:"+ me +",time:"+str(tick)+",userid:"+userid+",usertoken:"+usertoken+",appkey:"+appkey+",apptoken:"+apptoken 618 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 619 | send_values = { 620 | "id": 1, 621 | "method": "EpGet", 622 | "system": { 623 | "ver": "1.0", 624 | "lang": "en", 625 | "userid": userid, 626 | "appkey": appkey, 627 | "time": tick, 628 | "sign": sign 629 | }, 630 | "params": { 631 | "agt": agt, 632 | "me": me 633 | } 634 | } 635 | header = {'Content-Type': 'application/json'} 636 | send_data = json.dumps(send_values) 637 | req = urllib.request.Request(url=url, data=send_data.encode('utf-8'), headers=header, method='POST') 638 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 639 | return response['message']['data'] 640 | 641 | class LifeSmartStatesManager(threading.Thread): 642 | 643 | 644 | def __init__(self, ws): 645 | """Init LifeSmart Update Manager.""" 646 | threading.Thread.__init__(self) 647 | self._run = False 648 | self._lock = threading.Lock() 649 | self._ws = ws 650 | 651 | def run(self): 652 | while self._run: 653 | _LOGGER.debug('lifesmart: starting wss...') 654 | self._ws.run_forever() 655 | _LOGGER.debug('lifesmart: restart wss...') 656 | time.sleep(10) 657 | 658 | def start_keep_alive(self): 659 | """Start keep alive mechanism.""" 660 | with self._lock: 661 | self._run = True 662 | threading.Thread.start(self) 663 | 664 | def stop_keep_alive(self): 665 | """Stop keep alive mechanism.""" 666 | with self._lock: 667 | self._run = False 668 | self.join() 669 | -------------------------------------------------------------------------------- /lifesmart/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for LifeSmart binary sensors.""" 2 | import logging 3 | from homeassistant.components.binary_sensor import ( 4 | BinarySensorEntity, 5 | ENTITY_ID_FORMAT, 6 | ) 7 | 8 | from . import LifeSmartEntity 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | GUARD_SENSOR = ["SL_SC_G", 14 | "SL_SC_BG"] 15 | MOTION_SENSOR = ["SL_SC_MHW", 16 | "SL_SC_BM", 17 | "SL_SC_CM"] 18 | SMOKE_SENSOR = ["SL_P_A"] 19 | def setup_platform(hass, config, add_entities, discovery_info=None): 20 | """Perform the setup for lifesmart devices.""" 21 | if discovery_info is None: 22 | return 23 | dev = discovery_info.get("dev") 24 | param = discovery_info.get("param") 25 | devices = [] 26 | for idx in dev['data']: 27 | if idx in ["M","G","B","AXS","P1"]: 28 | devices.append(LifeSmartBinarySensor(dev,idx,dev['data'][idx],param)) 29 | add_entities(devices) 30 | 31 | class LifeSmartBinarySensor(LifeSmartEntity, BinarySensorEntity): 32 | """Representation of LifeSmartBinarySensor.""" 33 | 34 | def __init__(self, dev, idx, val, param): 35 | super().__init__(dev, idx, val, param) 36 | self.entity_id = ENTITY_ID_FORMAT.format(( dev['devtype'] + "_" + dev['agt'] + "_" + dev['me'] + "_" + idx).lower()) 37 | devtype = dev['devtype'] 38 | if devtype in GUARD_SENSOR: 39 | self._device_class = "door" 40 | elif devtype in MOTION_SENSOR: 41 | self._device_class = "motion" 42 | else: 43 | self._device_class = "smoke" 44 | if (val['val'] == 1 and self._device_class != "door") or (val['val'] == 0 and self._device_class == "door"): 45 | self._state = True 46 | else: 47 | self._state = False 48 | 49 | @property 50 | def is_on(self): 51 | """Return true if sensor is on.""" 52 | return self._state 53 | 54 | @property 55 | def device_class(self): 56 | """Return the class of binary sensor.""" 57 | return self._device_class 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /lifesmart/climate.py: -------------------------------------------------------------------------------- 1 | """Support for the LifeSmart climate devices.""" 2 | import logging 3 | import time 4 | from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity 5 | from homeassistant.components.climate.const import ( 6 | HVAC_MODE_AUTO, 7 | HVAC_MODE_COOL, 8 | HVAC_MODE_FAN_ONLY, 9 | HVAC_MODE_HEAT, 10 | HVAC_MODE_DRY, 11 | SUPPORT_FAN_MODE, 12 | SUPPORT_TARGET_TEMPERATURE, 13 | HVAC_MODE_OFF, 14 | ) 15 | 16 | from homeassistant.const import ( 17 | ATTR_TEMPERATURE, 18 | PRECISION_WHOLE, 19 | TEMP_CELSIUS, 20 | TEMP_FAHRENHEIT, 21 | ) 22 | 23 | from . import LifeSmartEntity 24 | _LOGGER = logging.getLogger(__name__) 25 | DEVICE_TYPE = "climate" 26 | 27 | LIFESMART_STATE_LIST = [HVAC_MODE_OFF, 28 | HVAC_MODE_AUTO, 29 | HVAC_MODE_FAN_ONLY, 30 | HVAC_MODE_COOL, 31 | HVAC_MODE_HEAT, 32 | HVAC_MODE_DRY] 33 | 34 | LIFESMART_STATE_LIST2 = [HVAC_MODE_OFF, 35 | HVAC_MODE_HEAT] 36 | 37 | SPEED_OFF = "Speed_Off" 38 | SPEED_LOW = "Speed_Low" 39 | SPEED_MEDIUM = "Speed_Medium" 40 | SPEED_HIGH = "Speed_High" 41 | 42 | FAN_MODES = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] 43 | GET_FAN_SPEED = { SPEED_LOW:15, SPEED_MEDIUM:45, SPEED_HIGH:76 } 44 | 45 | AIR_TYPES=["V_AIR_P"] 46 | 47 | THER_TYPES = ["SL_CP_DN"] 48 | 49 | 50 | LIFESMART_STATE_LIST 51 | 52 | def setup_platform(hass, config, add_entities, discovery_info=None): 53 | """Set up LifeSmart Climate devices.""" 54 | if discovery_info is None: 55 | return 56 | devices = [] 57 | dev = discovery_info.get("dev") 58 | param = discovery_info.get("param") 59 | devices = [] 60 | if "T" not in dev['data'] and "P3" not in dev['data']: 61 | return 62 | devices.append(LifeSmartClimateEntity(dev,"idx","0",param)) 63 | add_entities(devices) 64 | 65 | class LifeSmartClimateEntity(LifeSmartEntity, ClimateEntity): 66 | """LifeSmart climate devices,include air conditioner,heater.""" 67 | 68 | def __init__(self, dev, idx, val, param): 69 | """Init LifeSmart cover device.""" 70 | super().__init__(dev, idx, val, param) 71 | self._name = dev['name'] 72 | cdata = dev['data'] 73 | #_LOGGER.info("climate.py_cdata: %s",str(cdata)) 74 | self.entity_id = ENTITY_ID_FORMAT.format(( dev['devtype'] + "_" + dev['agt'] + "_" + dev['me']).lower().replace(":","_").replace("@","_")) 75 | if dev['devtype'] in AIR_TYPES: 76 | self._modes = LIFESMART_STATE_LIST 77 | if cdata['O']['type'] % 2 == 0: 78 | self._mode = LIFESMART_STATE_LIST[0] 79 | else: 80 | self._mode = LIFESMART_STATE_LIST[cdata['MODE']['val']] 81 | self._attributes.update({"last_mode": LIFESMART_STATE_LIST[cdata['MODE']['val']]}) 82 | _LOGGER.info("climate.py_self._attributes: %s",str(self._attributes)) 83 | self._current_temperature = cdata['T']['v'] 84 | self._target_temperature = cdata['tT']['v'] 85 | self._min_temp = 10 86 | self._max_temp = 35 87 | self._fanspeed = cdata['F']['val'] 88 | else: 89 | self._modes = LIFESMART_STATE_LIST2 90 | if cdata['P1']['type'] % 2 == 0: 91 | self._mode = LIFESMART_STATE_LIST2[0] 92 | else: 93 | self._mode = LIFESMART_STATE_LIST2[1] 94 | if cdata['P2']['type'] % 2 == 0: 95 | self._attributes.setdefault('Heating',"false") 96 | else: 97 | self._attributes.setdefault('Heating',"true") 98 | self._current_temperature = cdata['P4']['val'] / 10 99 | self._target_temperature = cdata['P3']['val'] / 10 100 | self._min_temp = 5 101 | self._max_temp = 35 102 | 103 | @property 104 | def precision(self): 105 | """Return the precision of the system.""" 106 | return PRECISION_WHOLE 107 | 108 | @property 109 | def temperature_unit(self): 110 | """Return the unit of measurement used by the platform.""" 111 | return TEMP_CELSIUS 112 | 113 | @property 114 | def hvac_mode(self): 115 | """Return current operation ie. heat, cool, idle.""" 116 | return self._mode 117 | 118 | @property 119 | def hvac_modes(self): 120 | """Return the list of available operation modes.""" 121 | return self._modes 122 | 123 | @property 124 | def current_temperature(self): 125 | """Return the current temperature.""" 126 | return self._current_temperature 127 | 128 | @property 129 | def target_temperature(self): 130 | """Return the temperature we try to reach.""" 131 | return self._target_temperature 132 | 133 | @property 134 | def target_temperature_step(self): 135 | """Return the supported step of target temperature.""" 136 | return 1 137 | 138 | @property 139 | def fan_mode(self): 140 | """Return the fan setting.""" 141 | fanmode = None 142 | if self._fanspeed < 30: 143 | fanmode = SPEED_LOW 144 | elif self._fanspeed < 65 and self._fanspeed >= 30: 145 | fanmode = SPEED_MEDIUM 146 | elif self._fanspeed >=65: 147 | fanmode = SPEED_HIGH 148 | return fanmode 149 | 150 | @property 151 | def fan_modes(self): 152 | """Return the list of available fan modes.""" 153 | return FAN_MODES 154 | 155 | def set_temperature(self, **kwargs): 156 | """Set new target temperature.""" 157 | new_temp = int(kwargs['temperature']*10) 158 | _LOGGER.info("set_temperature: %s",str(new_temp)) 159 | if self._devtype in AIR_TYPES: 160 | super()._lifesmart_epset(self, "0x88", new_temp, "tT") 161 | else: 162 | super()._lifesmart_epset(self, "0x88", new_temp, "P3") 163 | 164 | def set_fan_mode(self, fan_mode): 165 | """Set new target fan mode.""" 166 | super()._lifesmart_epset(self, "0xCE", GET_FAN_SPEED[fan_mode], "F") 167 | 168 | def set_hvac_mode(self, hvac_mode): 169 | """Set new target operation mode.""" 170 | if self._devtype in AIR_TYPES: 171 | if hvac_mode == HVAC_MODE_OFF: 172 | super()._lifesmart_epset(self, "0x80", 0, "O") 173 | return 174 | if self._mode == HVAC_MODE_OFF: 175 | if super()._lifesmart_epset(self, "0x81", 1, "O") == 0: 176 | time.sleep(2) 177 | else: 178 | return 179 | super()._lifesmart_epset(self, "0xCE", LIFESMART_STATE_LIST.index(hvac_mode), "MODE") 180 | else: 181 | if hvac_mode == HVAC_MODE_OFF: 182 | super()._lifesmart_epset(self, "0x80", 0, "P1") 183 | time.sleep(1) 184 | super()._lifesmart_epset(self, "0x80", 0, "P2") 185 | return 186 | else: 187 | if super()._lifesmart_epset(self, "0x81", 1, "P1") == 0: 188 | time.sleep(2) 189 | else: 190 | return 191 | 192 | 193 | 194 | @property 195 | def supported_features(self): 196 | """Return the list of supported features.""" 197 | if self._devtype in AIR_TYPES: 198 | return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE 199 | else: 200 | return SUPPORT_TARGET_TEMPERATURE 201 | 202 | @property 203 | def min_temp(self): 204 | """Return the minimum temperature.""" 205 | return self._min_temp 206 | 207 | @property 208 | def max_temp(self): 209 | """Return the maximum temperature.""" 210 | return self._max_temp 211 | -------------------------------------------------------------------------------- /lifesmart/cover.py: -------------------------------------------------------------------------------- 1 | """Support for LifeSmart covers.""" 2 | from homeassistant.components.cover import ( 3 | ENTITY_ID_FORMAT, 4 | ATTR_POSITION, 5 | CoverEntity, 6 | ) 7 | 8 | from . import LifeSmartEntity 9 | 10 | 11 | def setup_platform(hass, config, add_entities, discovery_info=None): 12 | """Set up lifesmart dooya cover devices.""" 13 | if discovery_info is None: 14 | return 15 | dev = discovery_info.get("dev") 16 | param = discovery_info.get("param") 17 | devices = [] 18 | idx = "P1" 19 | devices.append(LifeSmartCover(dev,idx,dev['data'][idx],param)) 20 | add_entities(devices) 21 | 22 | 23 | class LifeSmartCover(LifeSmartEntity, CoverEntity): 24 | """LifeSmart cover devices.""" 25 | 26 | def __init__(self, dev, idx, val, param): 27 | """Init LifeSmart cover device.""" 28 | super().__init__(dev, idx, val, param) 29 | self._name = dev['name'] 30 | self.entity_id = ENTITY_ID_FORMAT.format(( dev['devtype'] + "_" + dev['agt'] + "_" + dev['me']).lower()) 31 | self._pos = val['val'] 32 | self._device_class = "curtain" 33 | 34 | @property 35 | def current_cover_position(self): 36 | """Return the current position of the cover.""" 37 | return self._pos 38 | 39 | @property 40 | def is_closed(self): 41 | """Return if the cover is closed.""" 42 | return self.current_cover_position <= 0 43 | 44 | def close_cover(self, **kwargs): 45 | """Close the cover.""" 46 | super()._lifesmart_epset(self, "0xCF", 0, "P2") 47 | 48 | def open_cover(self, **kwargs): 49 | """Open the cover.""" 50 | super()._lifesmart_epset(self, "0xCF", 100, "P2") 51 | 52 | def stop_cover(self, **kwargs): 53 | """Stop the cover.""" 54 | super()._lifesmart_epset(self, "0xCE", 0x80, "P2") 55 | 56 | def set_cover_position(self, **kwargs): 57 | """Move the cover to a specific position.""" 58 | position = kwargs.get(ATTR_POSITION) 59 | super()._lifesmart_epset(self, "0xCE", position, "P2") 60 | 61 | @property 62 | def device_class(self): 63 | """Return the class of binary sensor.""" 64 | return self._device_class 65 | 66 | 67 | -------------------------------------------------------------------------------- /lifesmart/light.py: -------------------------------------------------------------------------------- 1 | """Support for LifeSmart Gateway Light.""" 2 | import binascii 3 | import logging 4 | import struct 5 | import urllib.request 6 | import json 7 | import time 8 | import hashlib 9 | from homeassistant.components.light import ( 10 | ATTR_BRIGHTNESS, 11 | ATTR_HS_COLOR, 12 | SUPPORT_BRIGHTNESS, 13 | SUPPORT_COLOR, 14 | LightEntity, 15 | ENTITY_ID_FORMAT, 16 | ) 17 | import homeassistant.util.color as color_util 18 | 19 | from . import LifeSmartEntity 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | 25 | def setup_platform(hass, config, add_entities, discovery_info=None): 26 | """Perform the setup for LifeSmart devices.""" 27 | if discovery_info is None: 28 | return 29 | dev = discovery_info.get("dev") 30 | param = discovery_info.get("param") 31 | devices = [] 32 | for idx in dev['data']: 33 | if idx in ["RGB","RGBW"]: 34 | devices.append(LifeSmartLight(dev,idx,dev['data'][idx],param)) 35 | add_entities(devices) 36 | 37 | class LifeSmartLight(LifeSmartEntity, LightEntity): 38 | """Representation of a LifeSmartLight.""" 39 | 40 | def __init__(self, dev, idx, val, param): 41 | """Initialize the LifeSmartLight.""" 42 | super().__init__(dev, idx, val, param) 43 | self.entity_id = ENTITY_ID_FORMAT.format(( dev['devtype'] + "_" + dev['agt'] + "_" + dev['me'] + "_" + idx).lower()) 44 | if val['type'] % 2 == 1: 45 | self._state = True 46 | else: 47 | self._state = False 48 | value = val['val'] 49 | if value == 0: 50 | self._hs = None 51 | else: 52 | rgbhexstr = "%x" % value 53 | rgbhexstr = rgbhexstr.zfill(8) 54 | rgbhex = bytes.fromhex(rgbhexstr) 55 | rgba = struct.unpack("BBBB", rgbhex) 56 | rgb = rgba[1:] 57 | self._hs = color_util.color_RGB_to_hs(*rgb) 58 | _LOGGER.info("hs_rgb: %s",str(self._hs)) 59 | 60 | 61 | async def async_added_to_hass(self): 62 | rmdata = {} 63 | rmlist = await self.hass.async_add_executor_job(LifeSmartLight._lifesmart_GetRemoteList,self) 64 | for ai in rmlist: 65 | rms = await self.hass.async_add_executor_job(LifeSmartLight._lifesmart_GetRemotes,self,ai) 66 | rms['category'] = rmlist[ai]['category'] 67 | rms['brand'] = rmlist[ai]['brand'] 68 | rmdata[ai] = rms 69 | self._attributes.setdefault('remotelist',rmdata) 70 | @property 71 | def is_on(self): 72 | """Return true if it is on.""" 73 | return self._state 74 | 75 | @property 76 | def hs_color(self): 77 | """Return the hs color value.""" 78 | return self._hs 79 | 80 | @property 81 | def supported_features(self): 82 | """Return the supported features.""" 83 | return SUPPORT_COLOR 84 | 85 | def turn_on(self, **kwargs): 86 | """Turn the light on.""" 87 | if ATTR_HS_COLOR in kwargs: 88 | self._hs = kwargs[ATTR_HS_COLOR] 89 | 90 | rgb = color_util.color_hs_to_RGB(*self._hs) 91 | rgba = (0,) + rgb 92 | rgbhex = binascii.hexlify(struct.pack("BBBB", *rgba)).decode("ASCII") 93 | rgbhex = int(rgbhex, 16) 94 | 95 | if super()._lifesmart_epset(self, "0xff", rgbhex, self._idx) == 0: 96 | self._state = True 97 | self.schedule_update_ha_state() 98 | 99 | def turn_off(self, **kwargs): 100 | """Turn the light off.""" 101 | if super()._lifesmart_epset(self, "0x80", 0, self._idx) == 0: 102 | self._state = False 103 | self.schedule_update_ha_state() 104 | @staticmethod 105 | def _lifesmart_GetRemoteList(self): 106 | appkey = self._appkey 107 | apptoken = self._apptoken 108 | usertoken = self._usertoken 109 | userid = self._userid 110 | agt = self._agt 111 | url = "https://api.ilifesmart.com/app/irapi.GetRemoteList" 112 | tick = int(time.time()) 113 | sdata = "method:GetRemoteList,agt:"+agt+",time:"+str(tick)+",userid:"+userid+",usertoken:"+usertoken+",appkey:"+appkey+",apptoken:"+apptoken 114 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 115 | send_values ={ 116 | "id": 1, 117 | "method": "GetRemoteList", 118 | "params": { 119 | "agt": agt 120 | }, 121 | "system": { 122 | "ver": "1.0", 123 | "lang": "en", 124 | "userid": userid, 125 | "appkey": appkey, 126 | "time": tick, 127 | "sign": sign 128 | } 129 | } 130 | header = {'Content-Type': 'application/json'} 131 | send_data = json.dumps(send_values) 132 | req = urllib.request.Request(url=url, data=send_data.encode('utf-8'), headers=header, method='POST') 133 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 134 | return response['message'] 135 | 136 | @staticmethod 137 | def _lifesmart_GetRemotes(self,ai): 138 | appkey = self._appkey 139 | apptoken = self._apptoken 140 | usertoken = self._usertoken 141 | userid = self._userid 142 | agt = self._agt 143 | url = "https://api.ilifesmart.com/app/irapi.GetRemote" 144 | tick = int(time.time()) 145 | sdata = "method:GetRemote,agt:"+agt+",ai:"+ai+",needKeys:2,time:"+str(tick)+",userid:"+userid+",usertoken:"+usertoken+",appkey:"+appkey+",apptoken:"+apptoken 146 | sign = hashlib.md5(sdata.encode(encoding='UTF-8')).hexdigest() 147 | send_values ={ 148 | "id": 1, 149 | "method": "GetRemote", 150 | "params": { 151 | "agt": agt, 152 | "ai": ai, 153 | "needKeys": 2 154 | }, 155 | "system": { 156 | "ver": "1.0", 157 | "lang": "en", 158 | "userid": userid, 159 | "appkey": appkey, 160 | "time": tick, 161 | "sign": sign 162 | } 163 | } 164 | header = {'Content-Type': 'application/json'} 165 | send_data = json.dumps(send_values) 166 | req = urllib.request.Request(url=url, data=send_data.encode('utf-8'), headers=header, method='POST') 167 | response = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 168 | return response['message']['codes'] 169 | -------------------------------------------------------------------------------- /lifesmart/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "lifesmart", 3 | "name": "lifesmart", 4 | "documentation": "https://github.com/skyzhishui/custom_components", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [ 8 | "@skyzhishui" 9 | ], 10 | "version": "23.10.26" 11 | } 12 | -------------------------------------------------------------------------------- /lifesmart/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for lifesmart sensors.""" 2 | import logging 3 | 4 | 5 | from homeassistant.const import ( 6 | TEMP_CELSIUS, 7 | ) 8 | DOMAIN = "sensor" 9 | ENTITY_ID_FORMAT = DOMAIN + ".{}" 10 | 11 | from . import LifeSmartEntity 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | GAS_SENSOR_TYPES = ["SL_SC_WA ", 16 | "SL_SC_CH", 17 | "SL_SC_CP", 18 | "ELIQ_EM"] 19 | 20 | OT_SENSOR_TYPES = ["SL_SC_MHW", 21 | "SL_SC_BM", 22 | "SL_SC_G", 23 | "SL_SC_BG"] 24 | 25 | 26 | def setup_platform(hass, config, add_entities, discovery_info=None): 27 | """Perform the setup for LifeSmart devices.""" 28 | devices = [] 29 | dev = discovery_info.get("dev") 30 | param = discovery_info.get("param") 31 | devices = [] 32 | for idx in dev['data']: 33 | if dev['devtype'] in OT_SENSOR_TYPES and idx in ["Z","V","P3","P4"]: 34 | devices.append(LifeSmartSensor(dev,idx,dev['data'][idx],param)) 35 | else: 36 | devices.append(LifeSmartSensor(dev,idx,dev['data'][idx],param)) 37 | add_entities(devices) 38 | 39 | 40 | class LifeSmartSensor(LifeSmartEntity): 41 | """Representation of a LifeSmartSensor.""" 42 | 43 | def __init__(self, dev, idx, val, param): 44 | """Initialize the LifeSmartSensor.""" 45 | super().__init__(dev, idx, val, param) 46 | self.entity_id = ENTITY_ID_FORMAT.format(( dev['devtype'] + "_" + dev['agt'] + "_" + dev['me'] + "_" + idx).lower()) 47 | devtype = dev['devtype'] 48 | if devtype in GAS_SENSOR_TYPES: 49 | self._unit = "None" 50 | self._device_class = "None" 51 | self._state = val['val'] 52 | else: 53 | if idx == "T" or idx == "P1": 54 | self._device_class = "temperature" 55 | self._unit = TEMP_CELSIUS 56 | elif idx == "H" or idx == "P2": 57 | self._device_class = "humidity" 58 | self._unit = "%" 59 | elif idx == "Z": 60 | self._device_class = "illuminance" 61 | self._unit = "lx" 62 | elif idx == "V": 63 | self._device_class = "battery" 64 | self._unit = "%" 65 | elif idx == "P3": 66 | self._device_class = "None" 67 | self._unit = "ppm" 68 | elif idx == "P4": 69 | self._device_class = "None" 70 | self._unit = "mg/m3" 71 | else: 72 | self._unit = "None" 73 | self._device_class = "None" 74 | self._state = val['v'] 75 | 76 | 77 | @property 78 | def unit_of_measurement(self): 79 | """Return the unit of measurement of this entity, if any.""" 80 | return self._unit 81 | 82 | @property 83 | def device_class(self): 84 | """Return the device class of this entity.""" 85 | return self._device_class 86 | 87 | @property 88 | def state(self): 89 | """Return the state of the sensor.""" 90 | return self._state 91 | -------------------------------------------------------------------------------- /lifesmart/services.yaml: -------------------------------------------------------------------------------- 1 | send_keys: 2 | description: send keys. 3 | fields: 4 | me: 5 | description: test. 6 | example: '0010' 7 | agt: 8 | description: test2. 9 | example: '_xXXXXXXXXXXXXXXXXX' 10 | ai: 11 | description: test3 12 | example: 'AI_IR_xxxx_xxxxxxxx' 13 | category: 14 | description: test4 15 | example: 'custom' 16 | brand: 17 | description: test5 18 | example: 'custom' 19 | keys: 20 | description: test6 21 | example: '["key"]' 22 | -------------------------------------------------------------------------------- /lifesmart/switch.py: -------------------------------------------------------------------------------- 1 | """lifesmart switch @skyzhishui""" 2 | import subprocess 3 | import urllib.request 4 | import json 5 | import time 6 | import hashlib 7 | import logging 8 | from . import LifeSmartEntity 9 | 10 | 11 | from homeassistant.components.switch import ( 12 | SwitchEntity, 13 | ENTITY_ID_FORMAT, 14 | ) 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | def setup_platform(hass, config, add_entities, discovery_info=None): 20 | """Find and return lifesmart switches.""" 21 | if discovery_info is None: 22 | return 23 | dev = discovery_info.get("dev") 24 | param = discovery_info.get("param") 25 | devices = [] 26 | for idx in dev['data']: 27 | if idx in ["L1","L2","L3","P1","P2","P3"]: 28 | devices.append(LifeSmartSwitch(dev,idx,dev['data'][idx],param)) 29 | add_entities(devices) 30 | return True 31 | 32 | class LifeSmartSwitch(LifeSmartEntity, SwitchEntity): 33 | 34 | 35 | def __init__(self, dev, idx, val, param): 36 | """Initialize the switch.""" 37 | super().__init__(dev, idx, val, param) 38 | self.entity_id = ENTITY_ID_FORMAT.format(( dev['devtype'] + "_" + dev['agt'] + "_" + dev['me'] + "_" + idx).lower()) 39 | if val['type'] %2 == 1: 40 | self._state = True 41 | else: 42 | self._state = False 43 | 44 | @property 45 | def is_on(self): 46 | """Return true if device is on.""" 47 | return self._state 48 | 49 | async def async_added_to_hass(self): 50 | """Call when entity is added to hass.""" 51 | 52 | def _get_state(self): 53 | """get lifesmart switch state.""" 54 | return self._state 55 | 56 | def turn_on(self, **kwargs): 57 | """Turn the device on.""" 58 | if super()._lifesmart_epset(self, "0x81", 1, self._idx) == 0: 59 | self._state = True 60 | self.schedule_update_ha_state() 61 | 62 | def turn_off(self, **kwargs): 63 | """Turn the device off.""" 64 | if super()._lifesmart_epset(self, "0x80", 0, self._idx) == 0: 65 | self._state = False 66 | self.schedule_update_ha_state() 67 | 68 | --------------------------------------------------------------------------------