From d10928ecca933065a1ccedf7584a09e81fb5d08e Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Wed, 8 Jun 2022 11:08:19 +0200 Subject: [PATCH] Remove local pyvesync lib and migrate to public one. (#25) * move to pip pyvesync library * remove pyvesync * improve extra attributes * fix style --- custom_components/vesync/__init__.py | 2 +- custom_components/vesync/common.py | 2 +- custom_components/vesync/config_flow.py | 2 +- custom_components/vesync/const.py | 2 + custom_components/vesync/fan.py | 31 +- custom_components/vesync/humidifier.py | 18 +- custom_components/vesync/pyvesync/__init__.py | 6 - .../__pycache__/__init__.cpython-39.pyc | Bin 336 -> 0 bytes .../__pycache__/helpers.cpython-39.pyc | Bin 6646 -> 0 bytes .../__pycache__/vesync.cpython-39.pyc | Bin 9396 -> 0 bytes .../vesyncbasedevice.cpython-39.pyc | Bin 4337 -> 0 bytes .../__pycache__/vesyncbulb.cpython-39.pyc | Bin 11505 -> 0 bytes .../__pycache__/vesyncfan.cpython-39.pyc | Bin 33848 -> 0 bytes .../__pycache__/vesyncoutlet.cpython-39.pyc | Bin 21101 -> 0 bytes .../__pycache__/vesyncswitch.cpython-39.pyc | Bin 11366 -> 0 bytes custom_components/vesync/pyvesync/helpers.py | 241 --- custom_components/vesync/pyvesync/vesync.py | 300 ---- .../vesync/pyvesync/vesyncbasedevice.py | 117 -- .../vesync/pyvesync/vesyncbulb.py | 378 ----- .../vesync/pyvesync/vesyncfan.py | 1299 ----------------- .../vesync/pyvesync/vesyncoutlet.py | 703 --------- .../vesync/pyvesync/vesyncswitch.py | 347 ----- requirements.txt | 1 + requirements_dev.txt | 3 +- 24 files changed, 29 insertions(+), 3423 deletions(-) delete mode 100755 custom_components/vesync/pyvesync/__init__.py delete mode 100644 custom_components/vesync/pyvesync/__pycache__/__init__.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/__pycache__/helpers.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/__pycache__/vesync.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/__pycache__/vesyncbasedevice.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/__pycache__/vesyncbulb.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/__pycache__/vesyncfan.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/__pycache__/vesyncoutlet.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/__pycache__/vesyncswitch.cpython-39.pyc delete mode 100644 custom_components/vesync/pyvesync/helpers.py delete mode 100755 custom_components/vesync/pyvesync/vesync.py delete mode 100644 custom_components/vesync/pyvesync/vesyncbasedevice.py delete mode 100644 custom_components/vesync/pyvesync/vesyncbulb.py delete mode 100644 custom_components/vesync/pyvesync/vesyncfan.py delete mode 100644 custom_components/vesync/pyvesync/vesyncoutlet.py delete mode 100644 custom_components/vesync/pyvesync/vesyncswitch.py create mode 100644 requirements.txt diff --git a/custom_components/vesync/__init__.py b/custom_components/vesync/__init__.py index fa66779..8ae086a 100644 --- a/custom_components/vesync/__init__.py +++ b/custom_components/vesync/__init__.py @@ -6,6 +6,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from pyvesync.vesync import VeSync from .common import async_process_devices from .const import ( @@ -21,7 +22,6 @@ from .const import ( VS_SENSORS, VS_SWITCHES, ) -from .pyvesync.vesync import VeSync PLATFORMS = { Platform.SWITCH: VS_SWITCHES, diff --git a/custom_components/vesync/common.py b/custom_components/vesync/common.py index b20f60d..be0e154 100644 --- a/custom_components/vesync/common.py +++ b/custom_components/vesync/common.py @@ -2,6 +2,7 @@ import logging from homeassistant.helpers.entity import Entity, ToggleEntity +from pyvesync.vesyncfan import model_features from .const import ( DOMAIN, @@ -13,7 +14,6 @@ from .const import ( VS_SENSORS, VS_SWITCHES, ) -from .pyvesync.vesyncfan import model_features _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/vesync/config_flow.py b/custom_components/vesync/config_flow.py index 6fb299c..03c3c88 100644 --- a/custom_components/vesync/config_flow.py +++ b/custom_components/vesync/config_flow.py @@ -8,9 +8,9 @@ from homeassistant.components import dhcp from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from pyvesync.vesync import VeSync from .const import DOMAIN -from .pyvesync.vesync import VeSync _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/vesync/const.py b/custom_components/vesync/const.py index fd9a8ce..820d29f 100644 --- a/custom_components/vesync/const.py +++ b/custom_components/vesync/const.py @@ -16,6 +16,8 @@ VS_MANAGER = "manager" VS_MODE_AUTO = "auto" VS_MODE_MANUAL = "manual" +VS_TO_HA_ATTRIBUTES = {"humidity": "current_humidity"} + DEV_TYPE_TO_HA = { "Core200S": "fan", "Core300S": "fan", diff --git a/custom_components/vesync/fan.py b/custom_components/vesync/fan.py index 1915257..367accd 100644 --- a/custom_components/vesync/fan.py +++ b/custom_components/vesync/fan.py @@ -14,7 +14,7 @@ from homeassistant.util.percentage import ( ) from .common import VeSyncDevice -from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_FANS +from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_FANS, VS_TO_HA_ATTRIBUTES _LOGGER = logging.getLogger(__name__) @@ -114,28 +114,13 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): def extra_state_attributes(self): """Return the state attributes of the fan.""" attr = {} - - if hasattr(self.smartfan, "active_time"): - attr["active_time"] = self.smartfan.active_time - - if hasattr(self.smartfan, "screen_status"): - attr["screen_status"] = self.smartfan.screen_status - - if hasattr(self.smartfan, "child_lock"): - attr["child_lock"] = self.smartfan.child_lock - - if hasattr(self.smartfan, "night_light"): - attr["night_light"] = self.smartfan.night_light - - if hasattr(self.smartfan, "air_quality"): - attr["air_quality"] = self.smartfan.air_quality - - if hasattr(self.smartfan, "mode"): - attr["mode"] = self.smartfan.mode - - if hasattr(self.smartfan, "filter_life"): - attr["filter_life"] = self.smartfan.filter_life - + for k, v in self.smarthumidifier.details.items(): + if k in VS_TO_HA_ATTRIBUTES: + attr[VS_TO_HA_ATTRIBUTES[k]] = v + elif k in self.state_attributes: + attr[f"vs_{k}"] = v + else: + attr[k] = v return attr def set_percentage(self, percentage): diff --git a/custom_components/vesync/humidifier.py b/custom_components/vesync/humidifier.py index c1e96d5..259452c 100644 --- a/custom_components/vesync/humidifier.py +++ b/custom_components/vesync/humidifier.py @@ -14,7 +14,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import VeSyncDevice, is_humidifier -from .const import DOMAIN, VS_DISCOVERY, VS_HUMIDIFIERS, VS_MODE_AUTO, VS_MODE_MANUAL +from .const import ( + DOMAIN, + VS_DISCOVERY, + VS_HUMIDIFIERS, + VS_MODE_AUTO, + VS_MODE_MANUAL, + VS_TO_HA_ATTRIBUTES, +) _LOGGER = logging.getLogger(__name__) @@ -22,7 +29,6 @@ MAX_HUMIDITY = 80 MIN_HUMIDITY = 30 MODES = [MODE_AUTO, MODE_NORMAL, MODE_SLEEP] -VESYNC_TO_HA_ATTRIBUTES = {"humidity": "current_humidity"} async def async_setup_entry( @@ -117,9 +123,11 @@ class VeSyncHumidifierHA(VeSyncDevice, HumidifierEntity): attr = {} for k, v in self.smarthumidifier.details.items(): - if k in VESYNC_TO_HA_ATTRIBUTES: - attr[VESYNC_TO_HA_ATTRIBUTES[k]] = v - elif k not in self.state_attributes: + if k in VS_TO_HA_ATTRIBUTES: + attr[VS_TO_HA_ATTRIBUTES[k]] = v + elif k in self.state_attributes: + attr[f"vs_{k}"] = v + else: attr[k] = v return attr diff --git a/custom_components/vesync/pyvesync/__init__.py b/custom_components/vesync/pyvesync/__init__.py deleted file mode 100755 index 3fb317f..0000000 --- a/custom_components/vesync/pyvesync/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""VeSync API Library.""" -import logging - -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)5s - %(message)s" -) diff --git a/custom_components/vesync/pyvesync/__pycache__/__init__.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 1c67091348ba2aa50d1e5d586809d54856c00d34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 336 zcmYjL%Sr<=6iudSJ1AZG1G6b~1e=vR5sOePNEHeq-Goe%j*&?+BA=?{KtD zAmGUY?#|MD$$5a)2OeH3niSR zWwi4obPsOmt&COF=P2|E;f4_s`jsh5smpL)P$${F(JNU7G@DPKK7dVQ)}egA;y0rN zZmI%%p=$gvVa5u~*3KJ68sXxf_;~%dWJc9S3+){~`E`352$5QPLdJC)(i>&?x)QfA OhPizrf@y+a@8=gshG94W diff --git a/custom_components/vesync/pyvesync/__pycache__/helpers.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/helpers.cpython-39.pyc deleted file mode 100644 index d6ae29ecf7ae92f0aace60f6530cd25f45f8cb47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6646 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 L*pNNcr@#3>*2(M- diff --git a/custom_components/vesync/pyvesync/__pycache__/vesync.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesync.cpython-39.pyc deleted file mode 100644 index 38eea686e10b7799f0d8f1277dbb46f323610947..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9396 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 E0jCZMp#T5? diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncbasedevice.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncbasedevice.cpython-39.pyc deleted file mode 100644 index 2f15ab39c46a8f9b7182b44b932a5f0cb1a74fbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4337 zcma)9TW{RP73T1^+^b|+mgLk;=`=~bhO?@JrYO>&b!yd#3RgBFJ17<{7;0x$ro<)J zL#|p8>%Pc8An#59=`nw4Ui;Kv5F|jGerL#)*Iu6*%W zFwUbi31j6vPB0nJi}}X}aR)8?J33)NID-W1Tf*k%R}hTb&+Wd$9qv9i`Yx|=4`Yqj zc>|-zo4kdwE*hfA+u-l;rRVku>$lEVmiZ;U(&j6g<1$~xi5;<|sVjU9)G}WIv~{&y z;vj%q8cK2Np-jg@=8?$!>`5pE_rrlq0sR8lU8ZVW= zBY-`o?8x|%$@^2-vvwRJUSi=Rc+LBJEs+UGKsiqfo~Y?|1ss0SRkyz!Sp1EulS6|Oj zz1g_~lg_Ljno|zaIp-QCxiMu2h_C~8Xr>#OBfA`!hvo@Gys+Yf$4Kg>u>RfcEkA_s zz!=p^G8yd&>F24Ri!ArW(+MIgKkjucW$&eFter-_fzNbJ`BMzaN%JQ#2I;o!-lbJH z2*lGM=wY5|Y0YB&0zAYsAh(}AG(XRvq&lDhlH5Eopl@Y+NEr8dmE!wyInW|H;U0Yzo+uce_MeP>3E7ax6>yTN#MV(gMOP!HhU8p#C z9~nCcehZEaH6k?EBF)XT2Mu26iRCVoGp-c*L?#Q0q}bJpq(h-3DD%*is0DW6kx-Uh zT>DJ#^m928el(n=mMHVnq^Ha@d33MK{=3bcC=1f$_c*yYhw_4*v1dAXFgGzrVa2TplfRsr)ySsip(XEdd)G)Bxp``F=ZvACZ~>@RbSprQ zDnN8^{S^hFKN<5d7k+qzx-Z;|MNe(a-)+Qme!s29V9j!U>CL~`c~soKOBH45P(57H zL3#Jx=u*}N?Vo?Jx-7GMh@l?=pj@TyC+L*J#olDUYw2Xvw+6$bz7-FTUXQ4y*;zp; z{C(^#Bg>+jW*Ik*ix?}a%IRf2+rrv7uRJD~wi7p=O;h%qe5p z7Z5Y3^E%#mLl}=4dE*qdsWzi|+B!3~jK943XY-l$ z%+66k^2Q6BH>Wmly|BN=*O>7%=9C%R#k&uY1fk4Sj>0@gIbUkr*I5#p&T^hk@?O!J z#VDo2qJ~mV#E8}oQe3rThoaXzk5uo0uE?9>#E8P$77Lj)f}X8To$F7YCe!$;6KS0l|)X2!X;DAm;iR2 z@)}09l#SsQ=x-}_sMvxp)aYFt9{&j+%_ylDmZvKY>hb%|t~cNj)Go-h)#g{+fG+Fv zOnVDNZk$r&Y18LqcISj>GB_FAL#uQ4Rqc#Q5wp1VhusG|{>Klu^{rKDdzNAmiOo6F zC`VhSXwBP2xusS5ZfO%`lSTR#?9pu~`yfNr?eNKHoc-T6xaeMLR^Un82rx5>L&LECzu55={GyfwGAdr4Kqg(xBh zWXsf=olBal+mw_p@M64!lQnwK{hB`JN%MM5a= Wqw+t)Z^(l(`omzGr57%n4eLMn-CK76 diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncbulb.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncbulb.cpython-39.pyc deleted file mode 100644 index ad23ca9714925586fa59fd3b68318bf5b2b552a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11505 zcmcIq+ix7#d7sfPZtZTCdxqpnyR-C} zS-G5&X=|x*iYARyw?JAe0NFRa=PxMo7xbYh27PLP0_tlY3J^is-}jxF*;($=bqbWy zoSiwhIp@3nzVA%CQYmWqlmB&V?bdUe_6vGQ|7G!T4VQO-;%!Z6O-<;+=;?i^s{49#M&w0d zLl?z|hA4@GC~p{iUwLRWYtF1w7uBnpm=U!Hnp4|5wy)jL?(5AtZkfHRImey3y%YPW z6SEuoNv(MjHFZ(Pm^02PXCD7fiQ^9~aYCGYkZCTwqKQ*t{(&auo$`INc^c0PB9G_1 za~jWQ@O)a7@Lb~Ov%YLH+|=Br|WOqxBG4B+i!F?Hhug0u(!7O7_`wEMwol~ z`tqBO-wvzoHP4srj^B6u&4E}!n{}hx@x#p9Zg=25)~UZ9=3aGrTaNT_TYJa3z3q0c zw>{^EbFbTRk{Z0==IuAGT)rIUMYrE?uk{>Em+cS4u;+yNb*JqQrQ?P5XojT!^1ETN zGw2Ou%Xj))G$x)${i3}bjcy4nu0yMe|4iIua4qBV7EpAx9qohq`^K)mYwYMdhS27< z&x{W(&_z?Cb-%HB_cfGq2S9S*dt%2*fzPA*$Z*506>Sv9}zO@||uky;#{iRcQ;eX=Nbx{IH#i=V7?Kuxzp+mHX~-oVAA zn8OWi{}jkE2hl&R?HDID5Wxr*u!nmNBm*q%VC}X)uHZuQ z=i^#MtrK_*D_B&^AGFj+>g&DH7R2cX zR`XQ!bVi)T(>%uK#5s&VFKj$7L~pntp1{*-^~C#qQapwIoc!s1ASf-|IvcBj|&JbQg0ZPmo;?k$q)3g~&a=S@h81!j=Mc<-a|wfowhPV%yE zkfGesT3`-J)3^3AJKCLWSKBxCGQ!xW+8kRtR?`>AnX z{}}AUg0sP;*W9k(ZTGr?6EDzX+eET&uO&NJl%(3SjAA7?yE3pltZhTt_F;`|&+&PW z&)OHg;Pe+XH_Bng8$!Ipe5=*PyIZZW)LN5d30=qY@Gvqsau%-(%?-y7OM*>W%iqRW zT1A-c4K_9~PsWjQAd?PmyYGbMw}!6Y?K?NA>5&;)p3_^GPtmYkzwNd$?vWlAS}o>I zt2I&{`7}B`gUhR;(2S~{#lI7JQLpO3u@QF{gw(`Gzly)g9~r5!LUa}um|h5GEz2MsTC$Z%x^6=->t~%N?IzGa2Y>p6Kpiu3=~Hb zUO}@*epoYW*iLXfwUJS;i9L`IvdJR5!FxED?BSem_$EEw$G>DRSE9Wvlhd$QVXJ8` zEKA*W8;su?NZ%25*R{W?>{_%pE3A+Bp6u=t-W!@-*JsNmU%=zxUCyS4DTDWQ^h!1h zuAND3cB1pdo)f;2dF4GXBzs=)4YlKBzh8~^%k$dgn;d8aoat!WZfwjKqpg~fRw!w2 z^0%m%wykPvr7-8ei+;(Tih3|VeNX6l^o9tfk>$9f-BaG?Y_xb^4jmipiG@oEouvM7 z%%p77mMDJ{4YzP9=VAt@Q)`N+ia9y4szkI%&4lg~srC7IWiK9D84FM(JswOC30Iof zoz1qp;dqOZW_x6?-H>0w{nUjM`M-%PUijJ6!c%0ARP5JWdMpvE5m65$ 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 CzJ*Ev diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncswitch.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncswitch.cpython-39.pyc deleted file mode 100644 index 5cca9643b50481adb57e340ad69f1c3e83c35996..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11366 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 diff --git a/custom_components/vesync/pyvesync/helpers.py b/custom_components/vesync/pyvesync/helpers.py deleted file mode 100644 index 6bc6bf0..0000000 --- a/custom_components/vesync/pyvesync/helpers.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Helper functions for VeSync API.""" - -import hashlib -import logging -import time - -import requests - -logger = logging.getLogger(__name__) - -API_BASE_URL = "https://smartapi.vesync.com" -API_RATE_LIMIT = 30 -API_TIMEOUT = 5 - -DEFAULT_TZ = "America/New_York" -DEFAULT_REGION = "US" - -APP_VERSION = "2.8.6" -PHONE_BRAND = "SM N9005" -PHONE_OS = "Android" -MOBILE_ID = "1234567890123456" -USER_TYPE = "1" -BYPASS_APP_V = "VeSync 3.0.51" - - -class Helpers: - """VeSync Helper Functions.""" - - @staticmethod - def req_headers(manager) -> dict: - """Build header for api requests.""" - return { - "accept-language": "en", - "accountId": manager.account_id, - "appVersion": APP_VERSION, - "content-type": "application/json", - "tk": manager.token, - "tz": manager.time_zone, - } - - @staticmethod - def req_body_base(manager) -> dict: - """Return universal keys for body of api requests.""" - return {"timeZone": manager.time_zone, "acceptLanguage": "en"} - - @staticmethod - def req_body_auth(manager) -> dict: - """Keys for authenticating api requests.""" - return {"accountID": manager.account_id, "token": manager.token} - - @staticmethod - def req_body_details() -> dict: - """Detail keys for api requests.""" - return { - "appVersion": APP_VERSION, - "phoneBrand": PHONE_BRAND, - "phoneOS": PHONE_OS, - "traceId": str(int(time.time())), - } - - @classmethod - def req_body(cls, manager, type_) -> dict: - """Builder for body of api requests.""" - body = {} - - if type_ == "login": - body = {**cls.req_body_base(manager), **cls.req_body_details()} - body["email"] = manager.username - body["password"] = cls.hash_password(manager.password) - body["devToken"] = "" - body["userType"] = USER_TYPE - body["method"] = "login" - elif type_ == "devicedetail": - body = { - **cls.req_body_base(manager), - **cls.req_body_auth(manager), - **cls.req_body_details(), - } - body["method"] = "devicedetail" - body["mobileId"] = MOBILE_ID - elif type_ == "devicelist": - body = { - **cls.req_body_base(manager), - **cls.req_body_auth(manager), - **cls.req_body_details(), - } - body["method"] = "devices" - body["pageNo"] = "1" - body["pageSize"] = "100" - elif type_ == "devicestatus": - body = {**cls.req_body_base(manager), **cls.req_body_auth(manager)} - elif type_ == "energy_week": - body = { - **cls.req_body_base(manager), - **cls.req_body_auth(manager), - **cls.req_body_details(), - } - body["method"] = "energyweek" - body["mobileId"] = MOBILE_ID - elif type_ == "energy_month": - body = { - **cls.req_body_base(manager), - **cls.req_body_auth(manager), - **cls.req_body_details(), - } - body["method"] = "energymonth" - body["mobileId"] = MOBILE_ID - elif type_ == "energy_year": - body = { - **cls.req_body_base(manager), - **cls.req_body_auth(manager), - **cls.req_body_details(), - } - body["method"] = "energyyear" - body["mobileId"] = MOBILE_ID - elif type_ == "bypass": - body = { - **cls.req_body_base(manager), - **cls.req_body_auth(manager), - **cls.req_body_details(), - } - body["method"] = "bypass" - elif type_ == "bypass_config": - body = { - **cls.req_body_base(manager), - **cls.req_body_auth(manager), - **cls.req_body_details(), - } - body["method"] = "firmwareUpdateInfo" - - return body - - @staticmethod - def calculate_hex(hex_string) -> float: - """Credit for conversion to itsnotlupus/vesync_wsproxy.""" - hex_conv = hex_string.split(":") - return (int(hex_conv[0], 16) + int(hex_conv[1], 16)) / 8192 - - @staticmethod - def hash_password(string) -> str: - """Encode password.""" - return hashlib.md5(string.encode("utf-8")).hexdigest() - - @staticmethod - def call_api( - api: str, method: str, json: dict = None, headers: dict = None - ) -> tuple: - """Make API calls by passing endpoint, header and body.""" - response = None - status_code = None - - try: - logger.debug("[%s] calling '%s' api", method, api) - if method.lower() == "get": - r = requests.get( - API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT - ) - elif method.lower() == "post": - r = requests.post( - API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT - ) - elif method.lower() == "put": - r = requests.put( - API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT - ) - except requests.exceptions.RequestException as e: - logger.debug(e) - except Exception as e: - logger.debug(e) - else: - if r.status_code == 200: - status_code = 200 - if r.content: - response = r.json() - else: - logger.debug("Unable to fetch %s%s", API_BASE_URL, api) - return response, status_code - - @staticmethod - def code_check(r: dict) -> bool: - """Test if code == 0 for successful API call.""" - return isinstance(r, dict) and r.get("code") == 0 - - @staticmethod - def build_details_dict(r: dict) -> dict: - """Build details dictionary from API response.""" - return { - "active_time": r.get("activeTime", 0), - "energy": r.get("energy", 0), - "night_light_status": r.get("nightLightStatus", None), - "night_light_brightness": r.get("nightLightBrightness", None), - "night_light_automode": r.get("nightLightAutomode", None), - "power": r.get("power", 0), - "voltage": r.get("voltage", 0), - } - - @staticmethod - def build_energy_dict(r: dict) -> dict: - """Build energy dictionary from API response.""" - return { - "energy_consumption_of_today": r.get("energyConsumptionOfToday", 0), - "cost_per_kwh": r.get("costPerKWH", 0), - "max_energy": r.get("maxEnergy", 0), - "total_energy": r.get("totalEnergy", 0), - "currency": r.get("currency", 0), - "data": r.get("data", 0), - } - - @staticmethod - def build_config_dict(r: dict) -> dict: - """Build configuration dictionary from API response.""" - if r.get("threshold") is not None: - threshold = r.get("threshold") - else: - threshold = r.get("thresHold") - return { - "current_firmware_version": r.get("currentFirmVersion"), - "latest_firmware_version": r.get("latestFirmVersion"), - "maxPower": r.get("maxPower"), - "threshold": threshold, - "power_protection": r.get("powerProtectionStatus"), - "energy_saving_status": r.get("energySavingStatus"), - } - - @classmethod - def bypass_body_v2(cls, manager): - """Build body dict for bypass calls.""" - bdy = {} - bdy.update(**cls.req_body(manager, "bypass")) - bdy["method"] = "bypassV2" - bdy["debugMode"] = False - bdy["deviceRegion"] = DEFAULT_REGION - return bdy - - @staticmethod - def bypass_header(): - """Build bypass header dict.""" - return { - "Content-Type": "application/json; charset=UTF-8", - "User-Agent": "okhttp/3.12.1", - } diff --git a/custom_components/vesync/pyvesync/vesync.py b/custom_components/vesync/pyvesync/vesync.py deleted file mode 100755 index 0b6f814..0000000 --- a/custom_components/vesync/pyvesync/vesync.py +++ /dev/null @@ -1,300 +0,0 @@ -"""VeSync API Device Library.""" - -import logging -import re -import time -from itertools import chain -from typing import Tuple - -from . import vesyncbulb as bulb_mods -from . import vesyncfan as fan_mods -from . import vesyncoutlet as outlet_mods -from . import vesyncswitch as switch_mods -from .helpers import Helpers -from .vesyncbasedevice import VeSyncBaseDevice - -logger = logging.getLogger(__name__) - -API_RATE_LIMIT: int = 30 -DEFAULT_TZ: str = "America/New_York" - -DEFAULT_ENER_UP_INT: int = 21600 - - -def object_factory(dev_type, config, manager) -> Tuple[str, VeSyncBaseDevice]: - """Get device type and instantiate class.""" - - def fans(dev_type, config, manager): - fan_cls = fan_mods.fan_modules[dev_type] - fan_obj = getattr(fan_mods, fan_cls) - return "fans", fan_obj(config, manager) - - def outlets(dev_type, config, manager): - outlet_cls = outlet_mods.outlet_modules[dev_type] - outlet_obj = getattr(outlet_mods, outlet_cls) - return "outlets", outlet_obj(config, manager) - - def switches(dev_type, config, manager): - switch_cls = switch_mods.switch_modules[dev_type] - switch_obj = getattr(switch_mods, switch_cls) - return "switches", switch_obj(config, manager) - - def bulbs(dev_type, config, manager): - bulb_cls = bulb_mods.bulb_modules[dev_type] - bulb_obj = getattr(bulb_mods, bulb_cls) - return "bulbs", bulb_obj(config, manager) - - if dev_type in fan_mods.fan_modules: - type_str, dev_obj = fans(dev_type, config, manager) - elif dev_type in outlet_mods.outlet_modules: - type_str, dev_obj = outlets(dev_type, config, manager) - elif dev_type in switch_mods.switch_modules: - type_str, dev_obj = switches(dev_type, config, manager) - elif dev_type in bulb_mods.bulb_modules: - type_str, dev_obj = bulbs(dev_type, config, manager) - else: - logger.debug( - "Unknown device named %s model %s", - config.get("deviceName", ""), - config.get("deviceType", ""), - ) - type_str = "unknown" - dev_obj = None - return type_str, dev_obj - - -class VeSync: - """VeSync API functions.""" - - def __init__(self, username, password, time_zone=DEFAULT_TZ, debug=False): - """Initialize VeSync class with username, password and time zone.""" - self.debug = debug - if debug: - logger.setLevel(logging.DEBUG) - self.username = username - self.password = password - self.token = None - self.account_id = None - self.devices = None - self.enabled = False - self.update_interval = API_RATE_LIMIT - self.last_update_ts = None - self.in_process = False - self._energy_update_interval = DEFAULT_ENER_UP_INT - self._energy_check = True - self._dev_list = {} - self.outlets = [] - self.switches = [] - self.fans = [] - self.bulbs = [] - self.scales = [] - - self._dev_list = { - "fans": self.fans, - "outlets": self.outlets, - "switches": self.switches, - "bulbs": self.bulbs, - } - - if isinstance(time_zone, str) and time_zone: - reg_test = r"[^a-zA-Z/_]" - if bool(re.search(reg_test, time_zone)): - self.time_zone = DEFAULT_TZ - logger.debug("Invalid characters in time zone - %s", time_zone) - else: - self.time_zone = time_zone - else: - self.time_zone = DEFAULT_TZ - logger.debug("Time zone is not a string") - - @property - def energy_update_interval(self) -> int: - """Return energy update interval.""" - return self._energy_update_interval - - @energy_update_interval.setter - def energy_update_interval(self, new_energy_update: int) -> None: - """Set energy update interval in seconds.""" - if new_energy_update > 0: - self._energy_update_interval = new_energy_update - - @staticmethod - def remove_dev_test(device, new_list: list) -> bool: - """Test if device should be removed - False = Remove.""" - if isinstance(new_list, list) and device.cid: - for item in new_list: - device_found = False - if "cid" in item: - if device.cid == item["cid"]: - device_found = True - break - else: - logger.debug("No cid found in - %s", str(item)) - if not device_found: - logger.debug( - "Device removed - %s - %s", device.device_name, device.device_type - ) - return False - return True - - def add_dev_test(self, new_dev: dict) -> bool: - """Test if new device should be added - True = Add.""" - if "cid" in new_dev: - for _, v in self._dev_list.items(): - for dev in v: - if ( - dev.cid == new_dev.get("cid") - and new_dev.get("subDeviceNo", 0) == dev.sub_device_no - ): - return False - return True - - def remove_old_devices(self, devices: list) -> bool: - """Remove devices not found in device list return.""" - for k, v in self._dev_list.items(): - before = len(v) - v[:] = [x for x in v if self.remove_dev_test(x, devices)] - after = len(v) - if before != after: - logger.debug("%s %s removed", str(before - after), k) - return True - - @staticmethod - def set_dev_id(devices: list) -> list: - """Correct devices without cid or uuid.""" - dev_rem = [] - for dev_num, dev in enumerate(devices): - if dev.get("cid") is None: - if dev.get("macID") is not None: - dev["cid"] = dev["macID"] - elif dev.get("uuid") is not None: - dev["cid"] = dev["uuid"] - else: - dev_rem.append(dev_num) - logger.warning("Device with no ID - %s", dev.get("deviceName")) - if dev_rem: - devices = [i for j, i in enumerate(devices) if j not in dev_rem] - return devices - - def process_devices(self, dev_list: list) -> bool: - """Instantiate Device Objects.""" - devices = VeSync.set_dev_id(dev_list) - - num_devices = sum( - len(v) if isinstance(v, list) else 1 for _, v in self._dev_list.items() - ) - - if not devices: - logger.warning("No devices found in api return") - return False - if num_devices == 0: - logger.debug("New device list initialized") - else: - self.remove_old_devices(devices) - - devices[:] = [x for x in devices if self.add_dev_test(x)] - - detail_keys = ["deviceType", "deviceName", "deviceStatus"] - for dev in devices: - if any(k not in dev for k in detail_keys): - logger.debug("Error adding device") - continue - dev_type = dev.get("deviceType") - try: - device_str, device_obj = object_factory(dev_type, dev, self) - device_list = getattr(self, device_str) - device_list.append(device_obj) - except AttributeError as err: - logger.debug("Error - %s", err) - logger.debug("%s device not added", dev_type) - continue - - return True - - def get_devices(self) -> bool: - """Return tuple listing outlets, switches, and fans of devices.""" - if not self.enabled: - return False - - self.in_process = True - proc_return = False - response, _ = Helpers.call_api( - "/cloud/v1/deviceManaged/devices", - "post", - headers=Helpers.req_headers(self), - json=Helpers.req_body(self, "devicelist"), - ) - - if response and Helpers.code_check(response): - if "result" in response and "list" in response["result"]: - device_list = response["result"]["list"] - if self.debug: - logger.debug(str(device_list)) - proc_return = self.process_devices(device_list) - else: - logger.error("Device list in response not found") - else: - logger.warning("Error retrieving device list") - - self.in_process = False - - return proc_return - - def login(self) -> bool: - """Return True if log in request succeeds.""" - user_check = isinstance(self.username, str) and len(self.username) > 0 - pass_check = isinstance(self.password, str) and len(self.password) > 0 - if not user_check: - logger.error("Username invalid") - return False - if not pass_check: - logger.error("Password invalid") - return False - - response, _ = Helpers.call_api( - "/cloud/v1/user/login", "post", json=Helpers.req_body(self, "login") - ) - - if Helpers.code_check(response) and "result" in response: - self.token = response.get("result").get("token") - self.account_id = response.get("result").get("accountID") - self.enabled = True - - return True - logger.error("Error logging in with username and password") - return False - - def device_time_check(self) -> bool: - """Test if update interval has been exceeded.""" - return ( - self.last_update_ts is None - or (time.time() - self.last_update_ts) > self.update_interval - ) - - def update(self) -> None: - """Fetch updated information about devices.""" - if self.device_time_check(): - - if not self.enabled: - logger.error("Not logged in to VeSync") - return - self.get_devices() - - devices = list(self._dev_list.values()) - - for device in chain(*devices): - device.update() - - self.last_update_ts = time.time() - - def update_energy(self, bypass_check=False) -> None: - """Fetch updated energy information about devices.""" - if self.outlets: - for outlet in self.outlets: - outlet.update_energy(bypass_check) - - def update_all_devices(self) -> None: - """Run get_details() for each device.""" - devices = list(self._dev_list.keys()) - for dev in chain(*devices): - dev.get_details() diff --git a/custom_components/vesync/pyvesync/vesyncbasedevice.py b/custom_components/vesync/pyvesync/vesyncbasedevice.py deleted file mode 100644 index f9799e4..0000000 --- a/custom_components/vesync/pyvesync/vesyncbasedevice.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Base class for all VeSync devices.""" - -import collections -import json -import logging -from typing import Optional, Union - -logger = logging.getLogger(__name__) - - -class VeSyncBaseDevice: - """Properties shared across all VeSync devices.""" - - def __init__(self, details: dict, manager): - """Initialize VeSync device base class.""" - self.manager = manager - if "cid" in details and details["cid"] is not None: - self.device_name: str = details.get("deviceName", None) - self.device_image: Optional[str] = details.get("deviceImg", None) - self.cid: str = details.get("cid", None) - self.connection_status: str = details.get("connectionStatus", None) - self.connection_type: Optional[str] = details.get("connectionType", None) - self.device_type: str = details.get("deviceType", None) - self.type: str = details.get("type", None) - self.uuid: Optional[str] = details.get("uuid", None) - self.config_module: str = details.get("configModule", None) - self.mac_id: Optional[str] = details.get("macID", None) - self.mode: Optional[str] = details.get("mode", None) - self.speed: Union[str, int, None] = details.get("speed", None) - self.extension = details.get("extension", None) - self.current_firm_version = details.get("currentFirmVersion", None) - self.sub_device_no = details.get("subDeviceNo", 0) - self.config: dict = {} - if isinstance(details.get("extension"), dict): - ext = details["extension"] - self.speed = ext.get("fanSpeedLevel") - self.mode = ext.get("mode") - if self.connection_status != "online": - self.device_status = "off" - else: - self.device_status = details.get("deviceStatus", None) - - else: - logger.error("No cid found for %s", self.__class__.__name__) - - def __eq__(self, other): - """Use device CID and subdevice number to test equality.""" - return bool(other.cid == self.cid and other.sub_device_no == self.sub_device_no) - - def __hash__(self): - """Use CID and sub-device number to make device hash.""" - if isinstance(self.sub_device_no, int) and self.sub_device_no > 0: - return hash(self.cid + str(self.sub_device_no)) - return hash(self.cid) - - def __str__(self): - """Use device info for string represtation of class.""" - return f"Device Name: {self.device_name}, \ - Device Type: {self.device_type},\ - SubDevice No.: {self.sub_device_no},\ - Status: {self.device_status}" - - def __repr__(self): - """Representation of device details.""" - return f"DevClass: {self.__class__.__name__},\ - Name:{self.device_name}, Device No: {self.sub_device_no},\ - DevStatus: {self.device_status}, CID: {self.cid}" - - @property - def is_on(self) -> bool: - """Return true if device is on.""" - if self.device_status == "on": - return True - return False - - @property - def firmware_update(self) -> bool: - """Return True if firmware update available.""" - cfv = self.config.get("current_firmware_version") - lfv = self.config.get("latest_firmware_version") - if cfv is not None and lfv is not None: - if cfv != lfv: - return True - else: - logger.debug("Call device.get_config() to get firmware versions") - return False - - def display(self) -> None: - """Print formatted device info to stdout.""" - disp = [ - ("Device Name:", self.device_name), - ("Model: ", self.device_type), - ("Subdevice No: ", str(self.sub_device_no)), - ("Status: ", self.device_status), - ("Online: ", self.connection_status), - ("Type: ", self.type), - ("CID: ", self.cid), - ] - if self.uuid is not None: - disp.append(("UUID: ", self.uuid)) - disp1 = collections.OrderedDict(disp) - for k, v in disp1.items(): - print(f"{k:.<15} {v:<15}") - - def displayJSON(self) -> str: - """JSON API for device details.""" - return json.dumps( - { - "Device Name": self.device_name, - "Model": self.device_type, - "Subdevice No": str(self.sub_device_no), - "Status": self.device_status, - "Online": self.connection_status, - "Type": self.type, - "CID": self.cid, - } - ) diff --git a/custom_components/vesync/pyvesync/vesyncbulb.py b/custom_components/vesync/pyvesync/vesyncbulb.py deleted file mode 100644 index 19fb115..0000000 --- a/custom_components/vesync/pyvesync/vesyncbulb.py +++ /dev/null @@ -1,378 +0,0 @@ -"""Etekcity Smart Light Bulb.""" - -import json -import logging -from abc import ABCMeta, abstractmethod -from typing import Dict, Union - -from .helpers import Helpers as helpers -from .vesyncbasedevice import VeSyncBaseDevice - -logger = logging.getLogger(__name__) - - -# Possible features - dimmable, color_temp, rgb_shift -feature_dict: dict = { - "ESL100": {"module": "VeSyncBulbESL100", "features": ["dimmable"]}, - "ESL100CW": { - "module": "VeSyncBulbESL100CW", - "features": ["dimmable", "color_temp"], - }, -} - - -bulb_modules: dict = {k: v["module"] for k, v in feature_dict.items()} - -__all__: list = list(bulb_modules.values()) + ["bulb_modules"] - - -def pct_to_kelvin(pct: float, max_k: int = 6500, min_k: int = 2700) -> float: - """Convert percent to kelvin.""" - return ((max_k - min_k) * pct / 100) + min_k - - -class VeSyncBulb(VeSyncBaseDevice): - """Base class for VeSync Bulbs.""" - - __metaclass__ = ABCMeta - - def __init__(self, details: Dict[str, Union[str, list]], manager): - """Initialize VeSync smart bulb base class.""" - super().__init__(details, manager) - self._brightness = 0 - self._color_temp = 0 - self.features = feature_dict.get(self.device_type, {}).get("features") - if self.features is None: - logger.error("No configuration set for - %s", self.device_name) - raise RuntimeError(f"No configuration set for - {self.device_name}") - - @property - def brightness(self) -> int: - """Return brightness of vesync bulb.""" - if self.dimmable_feature and self._brightness is not None: - return self._brightness - return 0 - - @property - def color_temp_kelvin(self) -> int: - """Return Color Temp of bulb if supported in Kelvin.""" - if self.color_temp_feature and self._color_temp is not None: - return int(pct_to_kelvin(self._color_temp)) - return 0 - - @property - def color_temp_pct(self) -> int: - """Return color temperature of bulb in percent.""" - if self.color_temp_feature and self._color_temp is not None: - return int(self._color_temp) - return 0 - - @property - def dimmable_feature(self) -> bool: - """Return true if dimmable bulb.""" - return "dimmable" in self.features - - @property - def color_temp_feature(self) -> bool: - """Return true if bulb supports color temperature changes.""" - return "color_temp" in feature_dict[self.device_type] - - @property - def rgb_shift_feature(self) -> bool: - """Return True if bulb supports changing color.""" - return "rgb_shift" in feature_dict[self.device_type] - - @abstractmethod - def get_details(self) -> None: - """Get vesync bulb details.""" - - @abstractmethod - def toggle(self, status: str) -> bool: - """Toggle vesync lightbulb.""" - - @abstractmethod - def get_config(self) -> None: - """Call api to get configuration details and firmware.""" - - def turn_on(self) -> bool: - """Turn on vesync bulbs.""" - return self._toggle("on") - - def turn_off(self) -> bool: - """Turn off vesync bulbs.""" - return self._toggle("off") - - def _toggle(self, state: str): - if self.toggle(state): - self.device_status = state - return True - logger.warning("Error turning %s %s", self.device_name, state) - return False - - def update(self) -> None: - """Update bulb details.""" - self.get_details() - - def display(self) -> None: - """Return formatted bulb info to stdout.""" - super().display() - if self.connection_status == "online" and self.dimmable_feature: - disp1 = [("Brightness: ", self.brightness, "%")] - for line in disp1: - print(f"{line[0]:.<17} {line[1]} {line[2]}") - - def displayJSON(self) -> str: - """Return bulb device info in JSON format.""" - sup = super().displayJSON() - sup_val = json.loads(sup) - if self.connection_status == "online": - if self.dimmable_feature: - sup_val.update({"Brightness": str(self.brightness)}) - if self.color_temp_feature: - sup_val.update({"Kelvin": str(self.color_temp_kelvin)}) - return sup_val - - -class VeSyncBulbESL100(VeSyncBulb): - """Object to hold VeSync ESL100 light bulb.""" - - def __init__(self, details, manager): - """Initialize Etekcity ESL100 Dimmable Bulb.""" - super().__init__(details, manager) - self.details: dict = {} - - def get_details(self) -> None: - """Get details of dimmable bulb.""" - body = helpers.req_body(self.manager, "devicedetail") - body["uuid"] = self.uuid - r, _ = helpers.call_api( - "/SmartBulb/v1/device/devicedetail", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - if helpers.code_check(r): - self.connection_status = r.get("connectionStatus") - self.device_status = r.get("deviceStatus") - if self.dimmable_feature: - self._brightness = int(r.get("brightNess")) - else: - logger.debug("Error getting %s details", self.device_name) - - def get_config(self) -> None: - """Get configuration of dimmable bulb.""" - body = helpers.req_body(self.manager, "devicedetail") - body["method"] = "configurations" - body["uuid"] = self.uuid - - r, _ = helpers.call_api( - "/SmartBulb/v1/device/configurations", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - - if helpers.code_check(r): - self.config = helpers.build_config_dict(r) - else: - logger.warning("Error getting %s config info", self.device_name) - - def toggle(self, status) -> bool: - """Toggle dimmable bulb.""" - body = self._get_body(status) - r, _ = helpers.call_api( - "/SmartBulb/v1/device/devicestatus", - "put", - headers=helpers.req_headers(self.manager), - json=body, - ) - if helpers.code_check(r): - self.device_status = status - return True - return False - - def set_brightness(self, brightness: int) -> bool: - """Set brightness of dimmable bulb.""" - if not self.dimmable_feature: - logger.debug("%s is not dimmable", self.device_name) - return False - if isinstance(brightness, int) and (brightness <= 0 or brightness > 100): - - logger.warning("Invalid brightness") - return False - - body = self._get_body("on") - body["brightNess"] = str(brightness) - r, _ = helpers.call_api( - "/SmartBulb/v1/device/updateBrightness", - "put", - headers=helpers.req_headers(self.manager), - json=body, - ) - - if helpers.code_check(r): - self._brightness = brightness - return True - - logger.debug("Error setting brightness for %s", self.device_name) - return False - - def _get_body(self, status: str): - body = helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = status - return body - - -class VeSyncBulbESL100CW(VeSyncBulb): - """VeSync Tunable and Dimmable White Bulb.""" - - def __init__(self, details, manager): - """Initialize Etekcity Tunable white bulb.""" - super().__init__(details, manager) - - def get_details(self) -> None: - """Get details of tunable bulb.""" - body = helpers.req_body(self.manager, "bypass") - body["cid"] = self.cid - body["jsonCmd"] = {"getLightStatus": "get"} - body["configModule"] = self.config_module - r, _ = helpers.call_api( - "/cloud/v1/deviceManaged/bypass", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - if not isinstance(r, dict) or not helpers.code_check(r): - logger.debug("Error calling %s", self.device_name) - return - response = r - if response.get("result", {}).get("light") is not None: - light = response.get("result", {}).get("light") - self.connection_status = "online" - self.device_status = light.get("action", "off") - if self.dimmable_feature: - self._brightness = light.get("brightness") - if self.color_temp_feature: - self._color_temp = light.get("colorTempe") - elif response.get("code") == -11300027: - logger.debug("%s device offline", self.device_name) - self.connection_status = "offline" - self.device_status = "off" - else: - logger.debug( - "%s - Unknown return code - %d with message %s", - self.device_name, - response.get("code"), - response.get("msg"), - ) - - def get_config(self) -> None: - """Get configuration and firmware info of tunable bulb.""" - body = helpers.req_body(self.manager, "bypass_config") - body["uuid"] = self.uuid - - r, _ = helpers.call_api( - "/cloud/v1/deviceManaged/configurations", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - - if helpers.code_check(r): - self.config = helpers.build_config_dict(r) - else: - logger.debug("Error getting %s config info", self.device_name) - - def toggle(self, status) -> bool: - """Toggle tunable bulb.""" - body = helpers.req_body(self.manager, "bypass") - body["cid"] = self.cid - body["configModule"] = self.config_module - body["jsonCmd"] = {"light": {"action": status}} - r, _ = helpers.call_api( - "/cloud/v1/deviceManaged/bypass", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - if helpers.code_check(r) == 0: - self.device_status = status - return True - logger.debug("%s offline", self.device_name) - self.device_status = "off" - self.connection_status = "offline" - return False - - def set_brightness(self, brightness: int) -> bool: - """Set brightness of tunable bulb.""" - if not self.dimmable_feature: - logger.debug("%s is not dimmable", self.device_name) - return False - if brightness <= 0 or brightness > 100: - logger.debug("Invalid brightness") - return False - - body = helpers.req_body(self.manager, "bypass") - body["cid"] = self.cid - body["configModule"] = self.config_module - if self.device_status == "off": - light_dict = {"action": "on", "brightness": brightness} - else: - light_dict = {"brightness": brightness} - body["jsonCmd"] = {"light": light_dict} - r, _ = helpers.call_api( - "/cloud/v1/deviceManaged/bypass", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - - if helpers.code_check(r): - self._brightness = brightness - return True - self.device_status = "off" - self.connection_status = "offline" - logger.debug("%s offline", self.device_name) - - return False - - def set_color_temp(self, color_temp: int) -> bool: - """Set Color Temperature of Bulb in pct (1 - 100).""" - if color_temp < 0 or color_temp > 100: - logger.debug("Invalid color temperature - only between 0 and 100") - return False - body = helpers.req_body(self.manager, "bypass") - body["cid"] = self.cid - body["jsonCmd"] = {"light": {}} - if self.device_status == "off": - light_dict = {"action": "on", "colorTempe": color_temp} - else: - light_dict = {"colorTempe": color_temp} - body["jsonCmd"]["light"] = light_dict - r, _ = helpers.call_api( - "/cloud/v1/deviceManaged/bypass", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - - if not helpers.code_check(r): - return False - - if r.get("code") == -11300027: - logger.debug("%s device offline", self.device_name) - self.connection_status = "offline" - self.device_status = "off" - return False - if r.get("code") == 0: - self.device_status = "on" - self._color_temp = color_temp - return True - logger.debug( - "%s - Unknown return code - %d with message %s", - self.device_name, - r.get("code"), - r.get("msg"), - ) - return False diff --git a/custom_components/vesync/pyvesync/vesyncfan.py b/custom_components/vesync/pyvesync/vesyncfan.py deleted file mode 100644 index e4e25b5..0000000 --- a/custom_components/vesync/pyvesync/vesyncfan.py +++ /dev/null @@ -1,1299 +0,0 @@ -"""VeSync API for controlling fans and purifiers.""" - -import json -import logging -from typing import Dict, Tuple, Union - -from .helpers import Helpers -from .vesyncbasedevice import VeSyncBaseDevice - -humid_features: dict = { - "Classic300S": { - "module": "VeSyncHumid200300S", - "models": ["Classic300S", "LUH-A601S-WUSB"], - "features": ["nightlight"], - "mist_modes": ["auto", "sleep", "manual"], - "mist_levels": list(range(1, 10)), - }, - "Classic200S": { - "module": "VeSyncHumid200S", - "models": ["Classic200S"], - "features": [], - "mist_modes": ["auto", "manual"], - "mist_levels": list(range(1, 10)), - }, - "Dual200S": { - "module": "VeSyncHumid200300S", - "models": ["Dual200S", "LUH-D301S-WUSR", "LUH-D301S-WJP", "LUH-D301S-WEU"], - "features": [], - "mist_modes": ["auto", "manual"], - "mist_levels": list(range(1, 3)), - }, - "LV600S": { - "module": "VeSyncHumid200300S", - "models": [ - "LUH-A602S-WUSR", - "LUH-A602S-WUS", - "LUH-A602S-WEUR", - "LUH-A602S-WEU", - "LUH-A602S-WJP", - ], - "features": ["warm_mist", "nightlight"], - "mist_modes": ["humidity", "sleep", "manual"], - "mist_levels": list(range(1, 10)), - "warm_mist_levels": [0, 1, 2, 3], - }, -} - - -air_features: dict = { - "Core200S": { - "module": "VeSyncAirBypass", - "models": ["Core200S", "LAP-C201S-AUSR", "LAP-C202S-WUSR"], - "modes": ["sleep", "off"], - "features": [], - "levels": list(range(1, 4)), - }, - "Core300S": { - "module": "VeSyncAirBypass", - "models": ["Core300S", "LAP-C301S-WJP"], - "modes": ["sleep", "off", "auto"], - "features": ["air_quality"], - "levels": list(range(1, 5)), - }, - "Core400S": { - "module": "VeSyncAirBypass", - "models": ["Core400S", "LAP-C401S-WJP", "LAP-C401S-WUSR", "LAP-C401S-WAAA"], - "modes": ["sleep", "off", "auto"], - "features": ["air_quality"], - "levels": list(range(1, 5)), - }, - "Core600S": { - "module": "VeSyncAirBypass", - "models": ["Core600S", "LAP-C601S-WUS", "LAP-C601S-WUSR", "LAP-C601S-WEU"], - "modes": ["sleep", "off", "auto"], - "features": ["air_quality"], - "levels": list(range(1, 5)), - }, - "LV-PUR131S": { - "module": "VeSyncAir131", - "models": ["LV-PUR131S", "LV-RH131S"], - "features": ["air_quality"], - }, -} - - -logger = logging.getLogger(__name__) - - -def model_dict() -> dict: - """Build purifier and humidifier model dictionary.""" - model_modules = {} - for dev_dict in {**air_features, **humid_features}.values(): - for model in dev_dict["models"]: - model_modules[model] = dev_dict["module"] - return model_modules - - -def model_features(dev_type: str) -> dict: - """Get features from device type.""" - for dev_dict in {**air_features, **humid_features}.values(): - if dev_type in dev_dict["models"]: - return dev_dict - raise ValueError("Device not configured") - - -fan_classes: set = {v["module"] for k, v in {**air_features, **humid_features}.items()} - -fan_modules: dict = model_dict() - -__all__: list = list(fan_classes) + ["fan_modules"] - - -class VeSyncAirBypass(VeSyncBaseDevice): - """Base class for Levoit Purifier Bypass API Calls.""" - - def __init__(self, details: Dict[str, list], manager): - """Initialize air devices.""" - super().__init__(details, manager) - self.enabled = True - self.config_dict = model_features(self.device_type) - self.features = self.config_dict.get("features", []) - if not isinstance(self.config_dict.get("modes"), list): - logger.error( - "Please set modes for %s in the configuration", self.device_type - ) - raise RuntimeError( - "Please set modes for %s in the configuration", self.device_type - ) - self.modes = self.config_dict["modes"] - self.air_quality_feature = "air_quality" in self.features - self.details: Dict[str, Union[str, int, float, bool]] = { - "filter_life": 0, - "mode": "manual", - "level": 0, - "display": False, - "child_lock": False, - "night_light": "off", - } - if self.air_quality_feature: - self.details["ait_quality"] = 0 - self.config: Dict[str, Union[str, int, float, bool]] = { - "display": False, - "display_forever": False, - } - - def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: - """Build device api body dictionary. - - standard modes are: ['getPurifierStatus', 'setSwitch', - 'setNightLight', - 'setLevel', 'setPurifierMode', 'setDisplay', - 'setChildLock'] - """ - modes = [ - "getPurifierStatus", - "setSwitch", - "setNightLight", - "setLevel", - "setPurifierMode", - "setDisplay", - "setChildLock", - "setIndicatorLight", - ] - if method not in modes: - logger.debug("Invalid mode - %s", method) - return {}, {} - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body["cid"] = self.cid - body["configModule"] = self.config_module - body["payload"] = {"method": method, "source": "APP"} - return head, body - - def build_purifier_dict(self, dev_dict: dict) -> None: - """Build Bypass purifier status dictionary.""" - self.enabled = dev_dict.get("enabled", False) - self.device_status = "on" if self.enabled else "off" - self.details["filter_life"] = dev_dict.get("filter_life", 0) - self.mode = dev_dict.get("mode", "manual") - self.speed = dev_dict.get("level", 0) - self.details["display"] = dev_dict.get("display", False) - self.details["child_lock"] = dev_dict.get("child_lock", False) - self.details["night_light"] = dev_dict.get("night_light", "off") - self.details["display"] = dev_dict.get("display", False) - self.details["display_forever"] = dev_dict.get("display_forever", False) - if self.air_quality_feature: - self.details["air_quality"] = dev_dict.get("air_quality", 0) - - def build_config_dict(self, conf_dict: Dict[str, str]) -> None: - """Build configuration dict for Bypass purifier.""" - self.config["display"] = conf_dict.get("display", False) - self.config["display_forever"] = conf_dict.get("display_forever", False) - - def get_details(self) -> None: - """Build Bypass Purifier details dictionary.""" - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body["cid"] = self.cid - body["configModule"] = self.config_module - body["payload"] = {"method": "getPurifierStatus", "source": "APP", "data": {}} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - if not isinstance(r, dict): - logger.debug("Error in purifier response") - return - if not isinstance(r.get("result"), dict): - logger.debug("Error in purifier response") - return - outer_result = r.get("result", {}) - inner_result = r.get("result", {}).get("result") if outer_result else None - if inner_result is not None and Helpers.code_check(r): - if outer_result.get("code") == 0: - self.build_purifier_dict(inner_result) - else: - logger.debug("error in inner result dict from purifier") - if inner_result.get("configuration", {}): - self.build_config_dict(inner_result.get("configuration", {})) - else: - logger.debug("No configuration found in purifier status") - else: - logger.debug("Error in purifier response") - - def update(self): - """Update Purifier details.""" - self.get_details() - - def change_fan_speed(self, speed=None) -> bool: - """Change fan speed based on levels in configuration dict.""" - speeds: list = self.config_dict.get("levels", []) - current_speed = self.speed - - if speed is not None: - if speed not in speeds: - logger.debug( - "%s is invalid speed - valid speeds are %s", speed, str(speeds) - ) - return False - new_speed = speed - if current_speed == new_speed: - return True - elif current_speed == speeds[-1]: - new_speed = speeds[0] - else: - current_index = speeds.index(current_speed) - new_speed = speeds[current_index + 1] - - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - - head, body = self.build_api_dict("setLevel") - if not head and not body: - return False - - body["payload"]["data"] = { - "id": 0, - "level": new_speed, - "type": "wind", - "mode": "manual", - } - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - self.speed = new_speed - return True - logger.debug("Error changing %s speed", self.device_name) - return False - - def child_lock_on(self) -> bool: - """Turn Bypass child lock on.""" - return self.set_child_lock(True) - - def child_lock_off(self) -> bool: - """Turn Bypass child lock off.""" - return self.set_child_lock(False) - - def set_child_lock(self, mode: bool) -> bool: - """Set Bypass child lock.""" - if mode not in (True, False): - logger.debug("Invalid mode passed to set_child_lock - %s", mode) - return False - - head, body = self.build_api_dict("setChildLock") - if not head and not body: - return False - - body["payload"]["data"] = {"child_lock": mode} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - self.details["child_lock"] = mode - return True - if isinstance(r, dict): - logger.debug("Error toggling child lock") - else: - logger.debug("Error in api return json for %s", self.device_name) - return False - - def mode_toggle(self, mode: str) -> bool: - """Set purifier mode - sleep or manual.""" - if mode.lower() not in self.modes: - logger.debug("Invalid purifier mode used - %s", mode) - return False - head, body = self.build_api_dict("setPurifierMode") - if not head and not body: - return False - - body["payload"]["data"] = {"mode": mode.lower()} - if mode == "manual": - body["payload"] = { - "data": {"id": 0, "level": 1, "type": "wind"}, - "method": "setLevel", - "type": "APP", - } - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if Helpers.code_check(r): - return True - logger.debug("Error setting purifier mode") - return False - - def manual_mode(self) -> bool: - """Set mode to manual.""" - if "manual" not in self.modes: - logger.debug("%s does not have manual mode", self.device_name) - return False - return self.mode_toggle("manual") - - def sleep_mode(self) -> bool: - """Set sleep mode to on.""" - if "sleep" not in self.modes: - logger.debug("%s does not have sleep mode", self.device_name) - return False - return self.mode_toggle("sleep") - - def auto_mode(self) -> bool: - """Set mode to auto.""" - if "auto" not in self.modes: - logger.debug("%s does not have auto mode", self.device_name) - return False - return self.mode_toggle("auto") - - def toggle_switch(self, toggle: bool) -> bool: - """Toggle purifier on/off.""" - if not isinstance(toggle, bool): - logger.debug("Invalid toggle value for purifier switch") - return False - - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body["cid"] = self.cid - body["configModule"] = self.config_module - body["payload"] = { - "data": {"enabled": toggle, "id": 0}, - "method": "setSwitch", - "source": "APP", - } - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - self.device_status = "on" if toggle else "off" - return True - logger.debug("Error toggling purifier - %s", self.device_name) - return False - - def turn_on(self) -> bool: - """Turn bypass Purifier on.""" - return self.toggle_switch(True) - - def turn_off(self): - """Turn Bypass Purifier off.""" - return self.toggle_switch(False) - - def set_display(self, mode: bool) -> bool: - """Toggle display on/off.""" - if not isinstance(mode, bool): - logger.debug("Mode must be True or False") - return False - - head, body = self.build_api_dict("setDisplay") - - body["payload"]["data"] = {"state": mode} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error toggling purifier display - %s", self.device_name) - return False - - def turn_on_display(self) -> bool: - """Turn Display on.""" - return self.set_display(True) - - def turn_off_display(self): - """Turn Display off.""" - return self.set_display(False) - - def set_night_light(self, mode: str) -> bool: - """Set night list - on, off or dim.""" - if mode.lower() not in ["on", "off", "dim"]: - logger.debug("Invalid nightlight mode used (on, off or dim)- %s", mode) - return False - head, body = self.build_api_dict("setNightLight") - if not head and not body: - return False - body["payload"]["data"] = {"night_light": mode.lower()} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - self.details["night_light"] = mode.lower() - return True - logger.debug("Error setting nightlight mode") - return False - - @property - def air_quality(self): - """Get air quality value (ug/m3).""" - if self.air_quality_feature is not True: - logger.debug("%s does not have air quality sensor", self.device_type) - try: - return int(self.details["air_quality"]) - except KeyError: - return 0 - - @property - def fan_level(self): - """Get current fan level (1-3).""" - try: - speed = int(self.speed) - except ValueError: - speed = self.speed - return speed - - @property - def filter_life(self) -> int: - """Get percentage of filter life remaining.""" - try: - return int(self.details["filter_life"]) - except KeyError: - return 0 - - @property - def display_state(self) -> bool: - """Get display state.""" - return bool(self.details["display"]) - - @property - def screen_status(self) -> bool: - """Get display status.""" - return bool(self.details["display"]) - - @property - def child_lock(self) -> bool: - """Get child lock state.""" - return bool(self.details["child_lock"]) - - @property - def night_light(self) -> str: - """Get night light state (on/dim/off).""" - return str(self.details["night_light"]) - - def display(self) -> None: - """Return formatted device info to stdout.""" - super().display() - disp1 = [ - ("Mode: ", self.mode, ""), - ("Filter Life: ", self.details["filter_life"], "percent"), - ("Fan Level: ", self.speed, ""), - ("Display: ", self.details["display"], ""), - ("Child Lock: ", self.details["child_lock"], ""), - ("Night Light: ", self.details["night_light"], ""), - ("Display Config: ", self.config["display"], ""), - ("Display_Forever Config: ", self.config["display_forever"], ""), - ] - if self.air_quality_feature: - disp1.append(("Air Quality: ", self.details["air_quality"], "ug/m3")) - for line in disp1: - print(f"{line[0]:.<20} {line[1]} {line[2]}") - - def displayJSON(self) -> str: - """Return air purifier status and properties in JSON output.""" - sup = super().displayJSON() - sup_val = json.loads(sup) - sup_val.update( - { - "Mode": self.mode, - "Filter Life": str(self.details["filter_life"]), - "Fan Level": str(self.speed), - "Display": self.details["display"], - "Child Lock": self.details["child_lock"], - "Night Light": str(self.details["night_light"]), - "Display Config": self.config["display"], - "Display_Forever Config": self.config["display_forever"], - } - ) - if self.air_quality_feature: - sup_val.update({"Air Quality": str(self.details["air_quality"])}) - return json.dumps(sup_val) - - -class VeSyncAir131(VeSyncBaseDevice): - """Levoit Air Purifier Class.""" - - def __init__(self, details, manager): - """Initialize air purifier class.""" - super().__init__(details, manager) - - self.details: Dict = {} - - def get_details(self) -> None: - """Build Air Purifier details dictionary.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["uuid"] = self.uuid - head = Helpers.req_headers(self.manager) - - r, _ = Helpers.call_api( - "/131airPurifier/v1/device/deviceDetail", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - self.device_status = r.get("deviceStatus", "unknown") - self.connection_status = r.get("connectionStatus", "unknown") - self.details["active_time"] = r.get("activeTime", 0) - self.details["filter_life"] = r.get("filterLife", {}) - self.details["screen_status"] = r.get("screenStatus", "unknown") - self.mode = r.get("mode", self.mode) - self.details["level"] = r.get("level", 0) - self.details["air_quality"] = r.get("airQuality", "unknown") - else: - logger.debug("Error getting %s details", self.device_name) - - def get_config(self) -> None: - """Get configuration info for air purifier.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["method"] = "configurations" - body["uuid"] = self.uuid - - r, _ = Helpers.call_api( - "/131airpurifier/v1/device/configurations", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if r is not None and Helpers.code_check(r): - self.config = Helpers.build_config_dict(r) - else: - logger.debug("Unable to get config info for %s", self.device_name) - - @property - def active_time(self) -> int: - """Return total time active in minutes.""" - return self.details.get("active_time", 0) - - @property - def fan_level(self) -> int: - """Get current fan level (1-3).""" - return self.details.get("level", 0) - - @property - def filter_life(self) -> int: - """Get percentage of filter life remaining.""" - try: - return self.details["filter_life"].get("percent", 0) - except KeyError: - return 0 - - @property - def air_quality(self) -> str: - """Get Air Quality.""" - return self.details.get("air_quality", "unknown") - - @property - def screen_status(self) -> str: - """Return Screen status (on/off).""" - return self.details.get("screen_status", "unknown") - - def turn_on(self) -> bool: - """Turn Air Purifier on.""" - if self.device_status == "on": - return False - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = "on" - head = Helpers.req_headers(self.manager) - - r, _ = Helpers.call_api( - "/131airPurifier/v1/device/deviceStatus", "put", json=body, headers=head - ) - - if r is not None and Helpers.code_check(r): - self.device_status = "on" - return True - logger.debug("Error turning %s on", self.device_name) - return False - - def turn_off(self) -> bool: - """Turn Air Purifier Off.""" - if self.device_status != "on": - return True - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = "off" - head = Helpers.req_headers(self.manager) - - r, _ = Helpers.call_api( - "/131airPurifier/v1/device/deviceStatus", "put", json=body, headers=head - ) - - if r is not None and Helpers.code_check(r): - self.device_status = "off" - return True - logger.debug("Error turning %s off", self.device_name) - return False - - def auto_mode(self) -> bool: - """Set mode to auto.""" - return self.mode_toggle("auto") - - def manual_mode(self) -> bool: - """Set mode to manual.""" - return self.mode_toggle("manual") - - def sleep_mode(self) -> bool: - """Set sleep mode to on.""" - return self.mode_toggle("sleep") - - def change_fan_speed(self, speed: int = None) -> bool: - """Adjust Fan Speed for air purifier. - - Specifying 1,2,3 as argument or call without argument to cycle - through speeds increasing by one. - """ - if self.mode != "manual": - logger.debug("%s not in manual mode, cannot change speed", self.device_name) - return False - - try: - level = self.details["level"] - except KeyError: - logger.debug( - "Cannot change fan speed, no level set for %s", self.device_name - ) - return False - - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - head = Helpers.req_headers(self.manager) - if speed is not None: - if speed == level: - return True - if speed in {1, 2, 3}: - body["level"] = speed - else: - logger.debug("Invalid fan speed for %s", self.device_name) - return False - else: - body["level"] = 1 if level > 2 else int(level + 1) - r, _ = Helpers.call_api( - "/131airPurifier/v1/device/updateSpeed", "put", json=body, headers=head - ) - - if r is not None and Helpers.code_check(r): - self.details["level"] = body["level"] - return True - logger.debug("Error changing %s speed", self.device_name) - return False - - def mode_toggle(self, mode: str) -> bool: - """Set mode to manual, auto or sleep.""" - head = Helpers.req_headers(self.manager) - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - if mode != self.mode and mode in {"sleep", "auto", "manual"}: - body["mode"] = mode - if mode == "manual": - body["level"] = 1 - - r, _ = Helpers.call_api( - "/131airPurifier/v1/device/updateMode", "put", json=body, headers=head - ) - - if r is not None and Helpers.code_check(r): - self.mode = mode - return True - - logger.debug("Error setting %s mode - %s", self.device_name, mode) - return False - - def update(self) -> None: - """Run function to get device details.""" - self.get_details() - - def display(self) -> None: - """Return formatted device info to stdout.""" - super().display() - disp1 = [ - ("Active Time : ", self.active_time, " minutes"), - ("Fan Level: ", self.fan_level, ""), - ("Air Quality: ", self.air_quality, ""), - ("Mode: ", self.mode, ""), - ("Screen Status: ", self.screen_status, ""), - ("Filter Life: ", self.filter_life, " percent"), - ] - for line in disp1: - print(f"{line[0]:.<15} {line[1]} {line[2]}") - - def displayJSON(self) -> str: - """Return air purifier status and properties in JSON output.""" - sup = super().displayJSON() - sup_val = json.loads(sup) - sup_val.update( - { - "Active Time": str(self.active_time), - "Fan Level": self.fan_level, - "Air Quality": self.air_quality, - "Mode": self.mode, - "Screen Status": self.screen_status, - "Filter Life": str(self.filter_life), - } - ) - return sup_val - - -class VeSyncHumid200300S(VeSyncBaseDevice): - """200S/300S Humidifier Class.""" - - def __init__(self, details, manager): - """Initialize 200S/300S Humidifier class.""" - super().__init__(details, manager) - self.enabled = True - self.config_dict = model_features(self.device_type) - self.mist_levels = self.config_dict.get("mist_levels") - self.mist_modes = self.config_dict.get("mist_modes") - self.features = self.config_dict.get("features") - if "warm_mist" in self.features: - self.warm_mist_levels = self.config_dict.get("warm_mist_levels", []) - self.warm_mist_feature = True - else: - self.warm_mist_feature = False - self.warm_mist_levels = [] - self.night_light = "nightlight" in self.config_dict.get("features", []) - self.details: Dict[str, Union[str, int, float]] = { - "humidity": 0, - "mist_virtual_level": 0, - "mist_level": 0, - "mode": "manual", - "water_lacks": False, - "humidity_high": False, - "water_tank_lifted": False, - "display": False, - "automatic_stop_reach_target": False, - } - if self.night_light: - self.details["night_light_brightness"] = 0 - self.config: Dict[str, Union[str, int, float]] = { - "auto_target_humidity": 0, - "display": False, - "automatic_stop": True, - } - self._api_modes = [ - "getHumidifierStatus", - "setAutomaticStop", - "setSwitch", - "setNightLightBrightness", - "setVirtualLevel", - "setTargetHumidity", - "setHumidityMode", - "setDisplay", - "setLevel", - ] - - def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: - """Build humidifier api call header and body. - - Available methods are: 'getHumidifierStatus', 'setAutomaticStop', - 'setSwitch', 'setNightLightBrightness', 'setVirtualLevel', - 'setTargetHumidity', 'setHumidityMode' - """ - if method not in self._api_modes: - logger.debug("Invalid mode - %s", method) - raise ValueError - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body["cid"] = self.cid - body["configModule"] = self.config_module - body["payload"] = {"method": method, "source": "APP"} - return head, body - - def build_humid_dict(self, dev_dict: Dict[str, str]) -> None: - """Build humidifier status dictionary.""" - self.enabled = dev_dict.get("enabled") - self.details["humidity"] = dev_dict.get("humidity", 0) - self.details["mist_virtual_level"] = dev_dict.get("mist_virtual_level", 0) - self.details["mist_level"] = dev_dict.get("mist_level", 0) - self.details["mode"] = dev_dict.get("mode", "manual") - self.details["water_lacks"] = dev_dict.get("water_lacks", False) - self.details["humidity_high"] = dev_dict.get("humidity_high", False) - self.details["water_tank_lifted"] = dev_dict.get("water_tank_lifted", False) - self.details["automatic_stop_reach_target"] = dev_dict.get( - "automatic_stop_reach_target", True - ) - if self.night_light: - self.details["night_light_brightness"] = dev_dict.get( - "night_light_brightness", 0 - ) - if self.warm_mist_feature: - self.details["warm_mist_level"] = dev_dict.get("warm_level", 0) - self.details["warm_mist_enabled"] = dev_dict.get("warm_enabled", False) - try: - self.details["display"] = dev_dict["display"] - except KeyError: - self.details["display"] = dev_dict.get("indicator_light_switch", False) - - def build_config_dict(self, conf_dict): - """Build configuration dict for 300s humidifier.""" - self.config["auto_target_humidity"] = conf_dict.get("auto_target_humidity", 0) - self.config["display"] = conf_dict.get("display", False) - self.config["automatic_stop"] = conf_dict.get("automatic_stop", True) - - def get_details(self) -> None: - """Build 200S/300S Humidifier details dictionary.""" - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body["cid"] = self.cid - body["configModule"] = self.config_module - body["payload"] = {"method": "getHumidifierStatus", "source": "APP", "data": {}} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - if r is None or not isinstance(r, dict): - logger.debug("Error getting status of %s ", self.device_name) - outer_result = None - else: - outer_result = r.get("result", {}) - inner_result = None - if outer_result is not None: - inner_result = r.get("result", {}).get("result") - if inner_result is not None and Helpers.code_check(r): - if outer_result.get("code") == 0: - self.build_humid_dict(inner_result) - else: - logger.debug("error in inner result dict from humidifier") - if inner_result.get("configuration", {}): - self.build_config_dict(inner_result.get("configuration", {})) - else: - logger.debug("No configuration found in humidifier status") - else: - logger.debug("Error in humidifier response") - - def update(self): - """Update 200S/300S Humidifier details.""" - self.get_details() - - def toggle_switch(self, toggle: bool) -> bool: - """Toggle humidifier on/off.""" - if not isinstance(toggle, bool): - logger.debug("Invalid toggle value for humidifier switch") - return False - - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body["cid"] = self.cid - body["configModule"] = self.config_module - body["payload"] = { - "data": {"enabled": toggle, "id": 0}, - "method": "setSwitch", - "source": "APP", - } - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - self.device_status = "on" if toggle else "off" - return True - logger.debug("Error toggling 300S humidifier - %s", self.device_name) - return False - - def turn_on(self) -> bool: - """Turn 200S/300S Humidifier on.""" - return self.toggle_switch(True) - - def turn_off(self): - """Turn 200S/300S Humidifier off.""" - return self.toggle_switch(False) - - def automatic_stop_on(self) -> bool: - """Turn 200S/300S Humidifier automatic stop on.""" - return self.set_automatic_stop(True) - - def automatic_stop_off(self) -> bool: - """Turn 200S/300S Humidifier automatic stop on.""" - return self.set_automatic_stop(False) - - def set_automatic_stop(self, mode: bool) -> bool: - """Set 200S/300S Humidifier to automatic stop.""" - if mode not in (True, False): - logger.debug("Invalid mode passed to set_automatic_stop - %s", mode) - return False - - head, body = self.build_api_dict("setAutomaticStop") - if not head and not body: - return False - - body["payload"]["data"] = {"enabled": mode} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - if isinstance(r, dict): - logger.debug("Error toggling automatic stop") - else: - logger.debug("Error in api return json for %s", self.device_name) - return False - - def set_display(self, mode: bool) -> bool: - """Toggle display on/off.""" - if not isinstance(mode, bool): - logger.debug("Mode must be True or False") - return False - - head, body = self.build_api_dict("setDisplay") - - body["payload"]["data"] = {"state": mode} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error toggling 300S display - %s", self.device_name) - return False - - def turn_on_display(self) -> bool: - """Turn 200S/300S Humidifier on.""" - return self.set_display(True) - - def turn_off_display(self): - """Turn 200S/300S Humidifier off.""" - return self.set_display(False) - - def set_humidity(self, humidity: int) -> bool: - """Set target 200S/300S Humidifier humidity.""" - if humidity < 30 or humidity > 80: - logger.debug("Humidity value must be set between 30 and 80") - return False - head, body = self.build_api_dict("setTargetHumidity") - - if not head and not body: - return False - - body["payload"]["data"] = {"target_humidity": humidity} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error setting humidity") - return False - - def set_night_light_brightness(self, brightness: int) -> bool: - """Set target 200S/300S Humidifier night light brightness.""" - if not self.night_light: - logger.debug( - "%s is a %s does not have a nightlight", - self.device_name, - self.device_type, - ) - return False - if brightness < 0 or brightness > 100: - logger.debug("Brightness value must be set between 0 and 100") - return False - head, body = self.build_api_dict("setNightLightBrightness") - - if not head and not body: - return False - - body["payload"]["data"] = {"night_light_brightness": brightness} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error setting humidity") - return False - - def set_humidity_mode(self, mode: str) -> bool: - """Set humidifier mode - sleep or auto.""" - if mode.lower() not in self.mist_modes: - logger.debug("Invalid humidity mode used - %s", mode) - logger.debug("Proper modes for this device are - %s", str(self.mist_modes)) - return False - head, body = self.build_api_dict("setHumidityMode") - if not head and not body: - return False - body["payload"]["data"] = {"mode": mode.lower()} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error setting humidity mode") - return False - - def set_warm_level(self, warm_level) -> bool: - """Set target 600S Humidifier mist warmth.""" - if not self.warm_mist_feature: - logger.debug( - "%s is a %s does not have a mist warmer", - self.device_name, - self.device_type, - ) - return False - if not isinstance(warm_level, int): - try: - warm_level = int(warm_level) - except ValueError: - logger.debug("Error converting warm mist level to a integer") - if warm_level not in self.warm_mist_levels: - logger.debug("warm_level value must be - %s", str(self.warm_mist_levels)) - return False - head, body = self.build_api_dict("setLevel") - - if not head and not body: - return False - - body["payload"]["data"] = { - "type": "warm", - "level": warm_level, - "id": 0, - } - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error setting warm") - return False - - def set_auto_mode(self): - """Set auto mode for humidifiers.""" - if "auto" in self.mist_modes: - call_str = "auto" - elif "humidity" in self.mist_modes: - call_str = "humidity" - else: - logger.debug( - "Trying auto mode, mode not set for this model, " - "please ensure %s model " - "is in configuration dictionary", - self.device_type, - ) - call_str = "auto" - return self.set_humidity_mode(call_str) - - def set_manual_mode(self): - """Set humifier to manual mode with 1 mist level.""" - return self.set_humidity_mode("manual") - - def set_mist_level(self, level) -> bool: - """Set humidifier mist level with int between 0 - 9.""" - try: - level = int(level) - except ValueError: - level = str(level) - if level not in self.mist_levels: - logger.debug("Humidifier mist level must be between 0 and 9") - return False - - head, body = self.build_api_dict("setVirtualLevel") - if not head and not body: - return False - - body["payload"]["data"] = {"id": 0, "level": level, "type": "mist"} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error setting mist level") - return False - - @property - def humidity(self): - """Get Humidity level.""" - return self.details["humidity"] - - @property - def mist_level(self): - """Get current mist level.""" - return self.details["virtual_mist_level"] - - @property - def water_lacks(self): - """If tank is empty return true.""" - return self.details["water_lacks"] - - @property - def auto_humidity(self): - """Auto target humidity.""" - return self.config["auto_target_humidity"] - - @property - def auto_enabled(self): - """Auto mode is enabled.""" - return self.details.get("mode") in ["auto", "humidity"] - - @property - def warm_mist_enabled(self): - """Warm mist feature enabled.""" - return self.details["warm_mist_enabled"] if self.warm_mist_feature else False - - def display(self) -> None: - """Return formatted device info to stdout.""" - super().display() - disp1 = [ - ("Mode: ", self.details["mode"], ""), - ("Humidity: ", self.details["humidity"], "percent"), - ("Mist Virtual Level: ", self.details["mist_virtual_level"], ""), - ("Mist Level: ", self.details["mist_level"], ""), - ("Water Lacks: ", self.details["water_lacks"], ""), - ("Humidity High: ", self.details["humidity_high"], ""), - ("Water Tank Lifted: ", self.details["water_tank_lifted"], ""), - ("Display: ", self.details["display"], ""), - ( - "Automatic Stop Reach Target: ", - self.details["automatic_stop_reach_target"], - "", - ), - ("Auto Target Humidity: ", self.config["auto_target_humidity"], "percent"), - ("Automatic Stop: ", self.config["automatic_stop"], ""), - ] - if self.night_light: - disp1.append( - ( - "Night Light Brightness: ", - self.details["night_light_brightness"], - "percent", - ) - ) - if self.warm_mist_feature: - disp1.append(("Warm mist enabled: ", self.details["warm_mist_enabled"], "")) - disp1.append(("Warm mist level: ", self.details["warm_mist_level"], "")) - for line in disp1: - print(f"{line[0]:.<29} {line[1]} {line[2]}") - - def displayJSON(self) -> str: - """Return air purifier status and properties in JSON output.""" - sup = super().displayJSON() - sup_val = json.loads(sup) - sup_val.update( - { - "Mode": self.details["mode"], - "Humidity": str(self.details["humidity"]), - "Mist Virtual Level": str(self.details["mist_virtual_level"]), - "Mist Level": str(self.details["mist_level"]), - "Water Lacks": self.details["water_lacks"], - "Humidity High": self.details["humidity_high"], - "Water Tank Lifted": self.details["water_tank_lifted"], - "Display": self.details["display"], - "Automatic Stop Reach Target": self.details[ - "automatic_stop_reach_target" - ], - "Auto Target Humidity": str(self.config["auto_target_humidity"]), - "Automatic Stop": self.config["automatic_stop"], - } - ) - if self.night_light: - sup_val["Night Light Brightness"] = self.details["night_light_brightness"] - if self.warm_mist_feature: - sup_val["Warm mist enabled"] = self.details["warm_mist_enabled"] - sup_val["Warm mist level"] = self.details["warm_mist_level"] - return json.dumps(sup_val) - - -class VeSyncHumid200S(VeSyncHumid200300S): - """Levoit Classic 200S Specific class.""" - - def __init__(self, details, manager): - """Initialize levoit 200S device class.""" - super().__init__(details, manager) - self._api_modes = [ - "getHumidifierStatus", - "setAutomaticStop", - "setSwitch", - "setVirtualLevel", - "setTargetHumidity", - "setHumidityMode", - "setIndicatorLightSwitch", - ] - - def set_display(self, mode: bool) -> bool: - """Toggle display on/off.""" - if not isinstance(mode, bool): - logger.debug("Mode must be True or False") - return False - - head, body = self.build_api_dict("setIndicatorLightSwitch") - - body["payload"]["data"] = {"enabled": mode, "id": 0} - - r, _ = Helpers.call_api( - "/cloud/v2/deviceManaged/bypassV2", - method="post", - headers=head, - json=body, - ) - - if r is not None and Helpers.code_check(r): - return True - logger.debug("Error toggling 300S display - %s", self.device_name) - return False diff --git a/custom_components/vesync/pyvesync/vesyncoutlet.py b/custom_components/vesync/pyvesync/vesyncoutlet.py deleted file mode 100644 index d9f45f8..0000000 --- a/custom_components/vesync/pyvesync/vesyncoutlet.py +++ /dev/null @@ -1,703 +0,0 @@ -"""Etekcity Outlets.""" - -import json -import logging -import time -from abc import ABCMeta, abstractmethod - -from .helpers import Helpers -from .vesyncbasedevice import VeSyncBaseDevice - -logger = logging.getLogger(__name__) - -outlet_config = { - "wifi-switch-1.3": {"module": "VeSyncOutlet7A"}, - "ESW03-USA": {"module": "VeSyncOutlet10A"}, - "ESW01-EU": {"module": "VeSyncOutlet10A"}, - "ESW15-USA": {"module": "VeSyncOutlet15A"}, - "ESO15-TB": {"module": "VeSyncOutdoorPlug"}, -} - -outlet_modules = {k: v["module"] for k, v in outlet_config.items()} - -__all__ = list(outlet_modules.values()) + ["outlet_modules"] - - -class VeSyncOutlet(VeSyncBaseDevice): - """Base class for Etekcity Outlets.""" - - __metaclass__ = ABCMeta - - def __init__(self, details, manager): - """Initialize VeSync Outlet base class.""" - super().__init__(details, manager) - - self.details = {} - self.energy = {} - self.update_energy_ts = None - self._energy_update_interval = manager.energy_update_interval - - @property - def update_time_check(self) -> bool: - """Test if energy update interval has been exceeded.""" - if self.update_energy_ts is None: - return True - - if (time.time() - self.update_energy_ts) > self._energy_update_interval: - return True - return False - - @abstractmethod - def turn_on(self) -> bool: - """Return True if device has beeeen turned on.""" - - @abstractmethod - def turn_off(self) -> bool: - """Return True if device has beeeen turned off.""" - - @abstractmethod - def get_details(self) -> None: - """Build details dictionary.""" - - @abstractmethod - def get_weekly_energy(self) -> None: - """Build weekly energy history dictionary.""" - - @abstractmethod - def get_monthly_energy(self) -> None: - """Build Monthly Energy History Dictionary.""" - - @abstractmethod - def get_yearly_energy(self): - """Build Yearly Energy Dictionary.""" - - @abstractmethod - def get_config(self): - """Get configuration and firmware details.""" - - def update(self): - """Get Device Energy and Status.""" - self.get_details() - - def update_energy(self, bypass_check: bool = False): - """Build weekly, monthly and yearly dictionaries.""" - if bypass_check or self.update_time_check: - self.update_energy_ts = time.time() - self.get_weekly_energy() - if "week" in self.energy: - self.get_monthly_energy() - self.get_yearly_energy() - if not bypass_check: - self.update_energy_ts = time.time() - - @property - def active_time(self) -> int: - """Return active time of a device in minutes.""" - return self.details.get("active_time", 0) - - @property - def energy_today(self) -> float: - """Return energy.""" - return self.details.get("energy", 0) - - @property - def power(self) -> float: - """Return current power in watts.""" - return float(self.details.get("power", 0)) - - @property - def voltage(self) -> float: - """Return current voltage.""" - return float(self.details.get("voltage", 0)) - - @property - def monthly_energy_total(self) -> float: - """Return total energy usage over the month.""" - return self.energy.get("month", {}).get("total_energy", 0) - - @property - def weekly_energy_total(self) -> float: - """Return total energy usage over the week.""" - return self.energy.get("week", {}).get("total_energy", 0) - - @property - def yearly_energy_total(self) -> float: - """Return total energy usage over the year.""" - return self.energy.get("year", {}).get("total_energy", 0) - - def display(self): - """Return formatted device info to stdout.""" - super().display() - disp1 = [ - ("Active Time : ", self.active_time, " minutes"), - ("Energy: ", self.energy_today, " kWh"), - ("Power: ", self.power, " Watts"), - ("Voltage: ", self.voltage, " Volts"), - ("Energy Week: ", self.weekly_energy_total, " kWh"), - ("Energy Month: ", self.monthly_energy_total, " kWh"), - ("Energy Year: ", self.yearly_energy_total, " kWh"), - ] - for line in disp1: - print(f"{line[0]:.<15} {line[1]} {line[2]}") - - def displayJSON(self): - """Return JSON details for outlet.""" - sup = super().displayJSON() - sup_val = json.loads(sup) - sup_val.update( - { - "Active Time": str(self.active_time), - "Energy": str(self.energy_today), - "Power": str(self.power), - "Voltage": str(self.voltage), - "Energy Week": str(self.weekly_energy_total), - "Energy Month": str(self.monthly_energy_total), - "Energy Year": str(self.yearly_energy_total), - } - ) - - return sup_val - - -class VeSyncOutlet7A(VeSyncOutlet): - """Etekcity 7A Round Outlet Class.""" - - def __init__(self, details, manager): - """Initialize Etekcity 7A round outlet class.""" - super().__init__(details, manager) - self.det_keys = ["deviceStatus", "activeTime", "energy", "power", "voltage"] - self.energy_keys = ["energyConsumptionOfToday", "maxEnergy", "totalEnergy"] - - def get_details(self) -> None: - """Get 7A outlet details.""" - r, _ = Helpers.call_api( - f"/v1/device/{self.cid}/detail", - "get", - headers=Helpers.req_headers(self.manager), - ) - - if r is not None and all(x in r for x in self.det_keys): - self.device_status = r.get("deviceStatus", self.device_status) - self.details["active_time"] = r.get("activeTime", 0) - self.details["energy"] = r.get("energy", 0) - power = r.get("power", "0:0") - power = round(float(Helpers.calculate_hex(power)), 2) - self.details["power"] = power - voltage = r.get("voltage", "0:0") - voltage = round(float(Helpers.calculate_hex(voltage)), 2) - self.details["voltage"] = voltage - else: - logger.debug("Unable to get %s details", self.device_name) - - def get_weekly_energy(self) -> None: - """Get 7A outlet weekly energy info and build weekly energy dict.""" - r, _ = Helpers.call_api( - f"/v1/device/{self.cid}/energy/week", - "get", - headers=Helpers.req_headers(self.manager), - ) - - if r is not None and all(x in r for x in self.energy_keys): - self.energy["week"] = Helpers.build_energy_dict(r) - else: - logger.debug("Unable to get %s weekly data", self.device_name) - - def get_monthly_energy(self) -> None: - """Get 7A outlet monthly energy info and build monthly energy dict.""" - r, _ = Helpers.call_api( - f"/v1/device/{self.cid}/energy/month", - "get", - headers=Helpers.req_headers(self.manager), - ) - - if r is not None and all(x in r for x in self.energy_keys): - self.energy["month"] = Helpers.build_energy_dict(r) - else: - logger.warning("Unable to get %s monthly data", self.device_name) - - def get_yearly_energy(self) -> None: - """Get 7A outlet yearly energy info and build yearly energy dict.""" - r, _ = Helpers.call_api( - f"/v1/device/{self.cid}/energy/year", - "get", - headers=Helpers.req_headers(self.manager), - ) - - if r is not None and all(x in r for x in self.energy_keys): - self.energy["year"] = Helpers.build_energy_dict(r) - else: - logger.debug("Unable to get %s yearly data", self.device_name) - - def turn_on(self) -> bool: - """Turn 7A outlet on - return True if successful.""" - return self._toggle("on") - - def turn_off(self) -> bool: - """Turn 7A outlet off - return True if successful.""" - return self._toggle("off") - - def _toggle(self, state): - _, status_code = Helpers.call_api( - f"/v1/wifi-switch-1.3/{self.cid}/status/{state}", - "put", - headers=Helpers.req_headers(self.manager), - ) - - if status_code is not None and status_code == 200: - self.device_status = state - return True - logger.warning("Error turning %s %s", self.device_name, state) - return False - - def get_config(self) -> None: - """Get 7A outlet configuration info.""" - r, _ = Helpers.call_api( - f"/v1/device/{self.cid}/configurations", - "get", - headers=Helpers.req_headers(self.manager), - ) - - if "currentFirmVersion" in r: - self.config = Helpers.build_config_dict(r) - else: - logger.debug("Error getting configuration info for %s", self.device_name) - - -class VeSyncOutlet10A(VeSyncOutlet): - """Etekcity 10A Round Outlets.""" - - def __init__(self, details, manager): - """Initialize 10A outlet class.""" - super().__init__(details, manager) - - def get_details(self) -> None: - """Get 10A outlet details.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["uuid"] = self.uuid - - r, _ = Helpers.call_api( - "/10a/v1/device/devicedetail", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(r): - self.device_status = r.get("deviceStatus", self.device_status) - self.connection_status = r.get("connectionStatus", self.connection_status) - self.details = Helpers.build_details_dict(r) - else: - logger.debug("Unable to get %s details", self.device_name) - - def get_config(self) -> None: - """Get 10A outlet configuration info.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["method"] = "configurations" - body["uuid"] = self.uuid - - r, _ = Helpers.call_api( - "/10a/v1/device/configurations", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(r): - self.config = Helpers.build_config_dict(r) - else: - logger.debug("Error getting %s config info", self.device_name) - - def get_weekly_energy(self) -> None: - """Get 10A outlet weekly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_week") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/10a/v1/device/energyweek", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["week"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s weekly data", self.device_name) - - def get_monthly_energy(self) -> None: - """Get 10A outlet monthly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_month") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/10a/v1/device/energymonth", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["month"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s monthly data", self.device_name) - - def get_yearly_energy(self) -> None: - """Get 10A outlet yearly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_year") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/10a/v1/device/energyyear", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["year"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s yearly data", self.device_name) - - def turn_on(self) -> bool: - """Turn 10A outlet on - return True if successful.""" - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = "on" - - response, _ = Helpers.call_api( - "/10a/v1/device/devicestatus", - "put", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.device_status = "on" - return True - logger.warning("Error turning %s on", self.device_name) - return False - - def turn_off(self) -> bool: - """Turn 10A outlet off - return True if successful.""" - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = "off" - - response, _ = Helpers.call_api( - "/10a/v1/device/devicestatus", - "put", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.device_status = "off" - return True - logger.warning("Error turning %s off", self.device_name) - return False - - -class VeSyncOutlet15A(VeSyncOutlet): - """Class for Etekcity 15A Rectangular Outlets.""" - - def __init__(self, details, manager): - """Initialize 15A rectangular outlets.""" - super().__init__(details, manager) - self.nightlight_status = "off" - self.nightlight_brightness = 0 - - def get_details(self) -> None: - """Get 15A outlet details.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["uuid"] = self.uuid - - r, _ = Helpers.call_api( - "/15a/v1/device/devicedetail", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - attr_list = ( - "deviceStatus", - "activeTime", - "energy", - "power", - "voltage", - "nightLightStatus", - "nightLightAutomode", - "nightLightBrightness", - ) - - if Helpers.code_check(r) and all(k in r for k in attr_list): - - self.device_status = r.get("deviceStatus") - self.connection_status = r.get("connectionStatus") - self.nightlight_status = r.get("nightLightStatus") - self.nightlight_brightness = r.get("nightLightBrightness") - self.details = Helpers.build_details_dict(r) - else: - logger.debug("Unable to get %s details", self.device_name) - - def get_config(self) -> None: - """Get 15A outlet configuration info.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["method"] = "configurations" - body["uuid"] = self.uuid - - r, _ = Helpers.call_api( - "/15a/v1/device/configurations", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(r): - self.config = Helpers.build_config_dict(r) - else: - logger.debug("Unable to get %s config info", self.device_name) - - def get_weekly_energy(self) -> None: - """Get 15A outlet weekly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_week") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/15a/v1/device/energyweek", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["week"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s weekly data", self.device_name) - - def get_monthly_energy(self) -> None: - """Get 15A outlet monthly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_month") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/15a/v1/device/energymonth", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["month"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s monthly data", self.device_name) - - def get_yearly_energy(self) -> None: - """Get 15A outlet yearly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_year") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/15a/v1/device/energyyear", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["year"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s yearly data", self.device_name) - - def turn_on(self) -> bool: - """Turn 15A outlet on - return True if successful.""" - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = "on" - - response, _ = Helpers.call_api( - "/15a/v1/device/devicestatus", - "put", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.device_status = "on" - return True - logger.warning("Error turning %s on", self.device_name) - return False - - def turn_off(self) -> bool: - """Turn 15A outlet off - return True if successful.""" - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = "off" - - response, _ = Helpers.call_api( - "/15a/v1/device/devicestatus", - "put", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.device_status = "off" - return True - - logger.warning("Error turning %s off", self.device_name) - return False - - def turn_on_nightlight(self) -> bool: - """Turn on nightlight.""" - return self._extracted_from_turn_off_nightlight_3("auto") - - def turn_off_nightlight(self) -> bool: - """Turn Off Nightlight.""" - return self._extracted_from_turn_off_nightlight_3("manual") - - def _extracted_from_turn_off_nightlight_3(self, mode): - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["mode"] = mode - response, _ = Helpers.call_api( - "/15a/v1/device/nightlightstatus", - "put", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - return True - logger.debug( - "Error turning %s %s nightlight", - "on" if mode == "auto" else "off", - self.device_name, - ) - return False - - -class VeSyncOutdoorPlug(VeSyncOutlet): - """Class to hold Etekcity outdoor outlets.""" - - def __init__(self, details, manager): - """Initialize Etekcity Outdoor Plug class.""" - super().__init__(details, manager) - - def get_details(self) -> None: - """Get details for outdoor outlet.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["uuid"] = self.uuid - r, _ = Helpers.call_api( - "/outdoorsocket15a/v1/device/devicedetail", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(r): - self.details = Helpers.build_details_dict(r) - self.connection_status = r.get("connectionStatus") - - dev_no = self.sub_device_no - sub_device_list = r.get("subDevices") - if sub_device_list and dev_no <= len(sub_device_list): - self.device_status = sub_device_list[(dev_no + -1)].get( - "subDeviceStatus" - ) - return - logger.debug("Unable to get %s details", self.device_name) - - def get_config(self) -> None: - """Get configuration info for outdoor outlet.""" - body = Helpers.req_body(self.manager, "devicedetail") - body["method"] = "configurations" - body["uuid"] = self.uuid - - r, _ = Helpers.call_api( - "/outdoorsocket15a/v1/device/configurations", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(r): - self.config = Helpers.build_config_dict(r) - logger.debug("Error getting %s config info", self.device_name) - - def get_weekly_energy(self) -> None: - """Get outdoor outlet weekly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_week") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/outdoorsocket15a/v1/device/energyweek", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["week"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s weekly data", self.device_name) - - def get_monthly_energy(self) -> None: - """Get outdoor outlet monthly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_month") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/outdoorsocket15a/v1/device/energymonth", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["month"] = Helpers.build_energy_dict(response) - logger.debug("Unable to get %s monthly data", self.device_name) - - def get_yearly_energy(self) -> None: - """Get outdoor outlet yearly energy info and populate energy dict.""" - body = Helpers.req_body(self.manager, "energy_year") - body["uuid"] = self.uuid - - response, _ = Helpers.call_api( - "/outdoorsocket15a/v1/device/energyyear", - "post", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.energy["year"] = Helpers.build_energy_dict(response) - else: - logger.debug("Unable to get %s yearly data", self.device_name) - - def toggle(self, status) -> bool: - """Toggle power for outdoor outlet.""" - body = Helpers.req_body(self.manager, "devicestatus") - body["uuid"] = self.uuid - body["status"] = status - body["switchNo"] = self.sub_device_no - - response, _ = Helpers.call_api( - "/outdoorsocket15a/v1/device/devicestatus", - "put", - headers=Helpers.req_headers(self.manager), - json=body, - ) - - if Helpers.code_check(response): - self.device_status = status - return True - logger.warning("Error turning %s %s", self.device_name, status) - return False - - def turn_on(self) -> bool: - """Turn outdoor outlet on and return True if successful.""" - return bool(self.toggle("on")) - - def turn_off(self) -> bool: - """Turn outdoor outlet off and return True if successful.""" - return bool(self.toggle("off")) diff --git a/custom_components/vesync/pyvesync/vesyncswitch.py b/custom_components/vesync/pyvesync/vesyncswitch.py deleted file mode 100644 index 5cca257..0000000 --- a/custom_components/vesync/pyvesync/vesyncswitch.py +++ /dev/null @@ -1,347 +0,0 @@ -"""Classes for VeSync Switch Devices.""" - -import json -import logging -from abc import ABCMeta, abstractmethod -from typing import Dict, Union - -from .helpers import Helpers as helpers -from .vesyncbasedevice import VeSyncBaseDevice - -logger = logging.getLogger(__name__) - -feature_dict: Dict[str, Dict[str, Union[list, str]]] = { - "ESWL01": {"module": "VeSyncWallSwitch", "features": []}, - "ESWD16": {"module": "VeSyncDimmerSwitch", "features": ["dimmable"]}, - "ESWL03": {"module": "VeSyncWallSwitch", "features": []}, -} - -switch_modules: dict = {k: v["module"] for k, v in feature_dict.items()} - -__all__: list = list(switch_modules.values()) + ["switch_modules"] - - -class VeSyncSwitch(VeSyncBaseDevice): - """Etekcity Switch Base Class.""" - - __metaclasss__ = ABCMeta - - def __init__(self, details, manager): - """Initialize Switch Base Class.""" - super().__init__(details, manager) - self.features = feature_dict.get(self.device_type, {}).get("features") - if self.features is None: - logger.error("No configuration set for - %s", self.device_name) - raise RuntimeError(f"No configuration set for - {self.device_name}") - self.details = {} - - def is_dimmable(self) -> bool: - """Return True if switch is dimmable.""" - return bool("dimmable" in self.features) - - @abstractmethod - def get_details(self) -> None: - """Get Device Details.""" - - @abstractmethod - def turn_on(self) -> bool: - """Turn Switch On.""" - - @abstractmethod - def turn_off(self) -> bool: - """Turn switch off.""" - - @abstractmethod - def get_config(self) -> None: - """Get configuration and firmware deatils.""" - - @property - def active_time(self) -> int: - """Get active time of switch.""" - return self.details.get("active_time", 0) - - def update(self) -> None: - """Update device details.""" - self.get_details() - - -class VeSyncWallSwitch(VeSyncSwitch): - """Etekcity standard wall switch class.""" - - def __init__(self, details, manager): - """Initialize standard etekcity wall switch class.""" - super().__init__(details, manager) - - def get_details(self) -> None: - """Get switch device details.""" - body = helpers.req_body(self.manager, "devicedetail") - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - - r, _ = helpers.call_api( - "/inwallswitch/v1/device/devicedetail", "post", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self.device_status = r.get("deviceStatus", self.device_status) - self.details["active_time"] = r.get("activeTime", 0) - self.connection_status = r.get("connectionStatus", self.connection_status) - else: - logger.debug("Error getting %s details", self.device_name) - - def get_config(self) -> None: - """Get switch device configuration info.""" - body = helpers.req_body(self.manager, "devicedetail") - body["method"] = "configurations" - body["uuid"] = self.uuid - - r, _ = helpers.call_api( - "/inwallswitch/v1/device/configurations", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - - if helpers.code_check(r): - self.config = helpers.build_config_dict(r) - else: - logger.warning("Unable to get %s config info", self.device_name) - - def turn_off(self) -> bool: - """Turn off switch device.""" - body = helpers.req_body(self.manager, "devicestatus") - body["status"] = "off" - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - - r, _ = helpers.call_api( - "/inwallswitch/v1/device/devicestatus", "put", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self.device_status = "off" - return True - logger.warning("Error turning %s off", self.device_name) - return False - - def turn_on(self) -> bool: - """Turn on switch device.""" - body = helpers.req_body(self.manager, "devicestatus") - body["status"] = "on" - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - - r, _ = helpers.call_api( - "/inwallswitch/v1/device/devicestatus", "put", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self.device_status = "on" - return True - logger.warning("Error turning %s on", self.device_name) - return False - - -class VeSyncDimmerSwitch(VeSyncSwitch): - """Vesync Dimmer Switch Class with RGB Faceplate.""" - - def __init__(self, details, manager): - """Initialize dimmer switch class.""" - super().__init__(details, manager) - self._brightness = 0 - self._rgb_value = {"red": 0, "blue": 0, "green": 0} - self._rgb_status = "unknown" - self._indicator_light = "unknown" - - def get_details(self) -> None: - """Get dimmer switch details.""" - body = helpers.req_body(self.manager, "devicedetail") - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - - r, _ = helpers.call_api( - "/dimmer/v1/device/devicedetail", "post", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self.device_status = r.get("deviceStatus", self.device_status) - self.details["active_time"] = r.get("activeTime", 0) - self.connection_status = r.get("connectionStatus", self.connection_status) - self._brightness = r.get("brightness") - self._rgb_status = r.get("rgbStatus") - self._rgb_value = r.get("rgbValue") - self._indicator_light = r.get("indicatorlightStatus") - else: - logger.debug("Error getting %s details", self.device_name) - - @property - def brightness(self) -> float: - """Return brightness in percent.""" - return self._brightness - - @property - def indicator_light_status(self) -> str: - """Faceplate brightness light status.""" - return self._indicator_light - - @property - def rgb_light_status(self) -> str: - """RGB Faceplate light status.""" - return self._rgb_status - - @property - def rgb_light_value(self) -> dict: - """RGB Light Values.""" - return self._rgb_value - - def switch_toggle(self, status: str) -> bool: - """Toggle switch status.""" - if status not in ["on", "off"]: - logger.debug("Invalid status passed to wall switch") - return False - body = helpers.req_body(self.manager, "devicestatus") - body["status"] = status - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - - r, _ = helpers.call_api( - "/dimmer/v1/device/devicestatus", "put", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self.device_status = status - return True - - logger.warning("Error turning %s %s", self.device_name, status) - return False - - def turn_on(self) -> bool: - """Turn switch on.""" - return self.switch_toggle("on") - - def turn_off(self) -> bool: - """Turn switch off.""" - return self.switch_toggle("off") - - def indicator_light_toggle(self, status: str) -> bool: - """Toggle indicator light.""" - if status not in ["on", "off"]: - logger.debug("Invalid status for wall switch") - return False - body = helpers.req_body(self.manager, "devicestatus") - body["status"] = status - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - - r, _ = helpers.call_api( - "/dimmer/v1/device/indicatorlightstatus", "put", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self.device_status = status - return True - - logger.warning("Error turning %s indicator light %s", self.device_name, status) - return False - - def indicator_light_on(self) -> bool: - """Turn Indicator light on.""" - return self.indicator_light_toggle("on") - - def indicator_light_off(self) -> bool: - """Turn indicator light off.""" - return self.indicator_light_toggle("off") - - def rgb_color_status( - self, status: str, red: int = None, blue: int = None, green: int = None - ) -> bool: - """Set faceplate RGB color.""" - body = helpers.req_body(self.manager, "devicestatus") - body["status"] = status - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - if red is not None and blue is not None and green is not None: - body["rgbValue"] = {"red": red, "blue": blue, "green": green} - - r, _ = helpers.call_api( - "/dimmer/v1/device/devicergbstatus", "put", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self._rgb_status = status - if body.get("rgbValue") is not None: - self._rgb_value = {"red": red, "blue": blue, "green": green} - return True - logger.warning("Error turning %s off", self.device_name) - return False - - def rgb_color_off(self) -> bool: - """Turn RGB Color Off.""" - return self.rgb_color_status("off") - - def rgb_color_on(self) -> bool: - """Turn RGB Color Off.""" - return self.rgb_color_status("on") - - def rgb_color_set(self, red: int, green: int, blue: int) -> bool: - """Set RGB color of faceplate.""" - if isinstance(red, int) and isinstance(green, int) and isinstance(blue, int): - for color in [red, green, blue]: - if color < 0 or color > 255: - logger.warning("Invalid RGB value") - return False - - return bool(self.rgb_color_status("on", red, green, blue)) - return False - - def set_brightness(self, brightness: int) -> bool: - """Set brightness of dimmer - 1 - 100.""" - if isinstance(brightness, int) and (brightness > 0 or brightness <= 100): - - body = helpers.req_body(self.manager, "devicestatus") - body["brightness"] = brightness - body["uuid"] = self.uuid - head = helpers.req_headers(self.manager) - - r, _ = helpers.call_api( - "/dimmer/v1/device/updatebrightness", "put", headers=head, json=body - ) - - if r is not None and helpers.code_check(r): - self._brightness = brightness - return True - logger.warning("Error setting %s brightness", self.device_name) - else: - logger.warning("Invalid brightness") - return False - - def displayJSON(self) -> str: - """JSON API for dimmer switch.""" - sup_val = json.loads(super().displayJSON()) - if self.is_dimmable: - sup_val.update( - { - "Indicator Light": str(self.active_time), - "Brightness": str(self._brightness), - "RGB Light": str(self._rgb_status), - } - ) - return sup_val - - def get_config(self) -> None: - """Get dimmable switch device configuration info.""" - body = helpers.req_body(self.manager, "devicedetail") - body["method"] = "configurations" - body["uuid"] = self.uuid - - r, _ = helpers.call_api( - "/dimmer/v1/device/configurations", - "post", - headers=helpers.req_headers(self.manager), - json=body, - ) - - if helpers.code_check(r): - self.config = helpers.build_config_dict(r) - else: - logger.warning("Unable to get %s config info", self.device_name) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..032e9e5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyvesync==2.0.3 \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index b5b86bf..d7b47f6 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,5 @@ -homeassistant +-r requirements.txt +homeassistant==2022.6.4 black isort flake8