2323 --version show program's version number and exit
2424 --debug print debug logs
2525"""
26+
2627import asyncio
2728import logging
2829import sys
30+ from collections .abc import Coroutine
2931from http import HTTPStatus
30- from typing import Any , Callable , Coroutine , Dict , List , NamedTuple , Optional , TypeVar , cast
32+ from typing import Any , Callable , NamedTuple , Optional , TypeVar , cast
3133
3234import httpx
3335from bs4 import BeautifulSoup , PageElement , Tag
@@ -73,13 +75,13 @@ class Status(NamedTuple):
7375
7476def _strip_unit (value : str ) -> str :
7577 """Strip unit from quantity and return only a quantity value."""
76- return value .strip ().partition (' ' )[0 ]
78+ return value .strip ().partition (" " )[0 ]
7779
7880
7981async def get_status (client : httpx .AsyncClient , device : str ) -> Status :
8082 """Return device status."""
8183 try :
82- response = await client .get (f' http://{ device } /' , timeout = 3 )
84+ response = await client .get (f" http://{ device } /" , timeout = 3 )
8385 response .raise_for_status ()
8486 except httpx .HTTPError as error :
8587 _LOGGER .debug ("Error encountered: %s" , error )
@@ -88,26 +90,35 @@ async def get_status(client: httpx.AsyncClient, device: str) -> Status:
8890
8991 try :
9092 content = BeautifulSoup (response .text , features = "html.parser" )
91- container = cast (Tag , content .find (class_ = 'container' ))
92- temperature_raw = cast (PageElement ,
93- cast (Tag , container .find_all (class_ = 'col-12' )[1 ]).find (class_ = 'bigText' )).text
94- in_data , _ , temp_out = temperature_raw .strip ().partition ('%' )
95- temp_in , _ , humi_in = in_data .strip ().partition ('/' )
96- mode_raw = cast (PageElement , cast (Tag , container .find_all (class_ = 'col-12' )[3 ]).find ('span' )).text
97- co2_raw = cast (PageElement , cast (Tag , container .find_all (class_ = 'col-12' )[4 ]).find ('b' )).text
98- filter_raw = cast (
99- str ,
100- cast (Tag , cast (Tag , container .find_all (class_ = 'filterBox' )[1 ]).div )['style' ],
101- ).partition (':' )[2 ].partition ('%' )[0 ]
102- fan_raw = cast (
103- str ,
104- cast (Tag , cast (Tag , container .find_all (class_ = 'filterBox' )[2 ]).div )['style' ],
105- ).partition (':' )[2 ].partition ('%' )[0 ]
106- light_raw = cast (str , cast (Tag , container .find (id = 'myRange' ))['value' ])
93+ container = cast (Tag , content .find (class_ = "container" ))
94+ temperature_raw = cast (
95+ PageElement , cast (Tag , container .find_all (class_ = "col-12" )[1 ]).find (class_ = "bigText" )
96+ ).text
97+ in_data , _ , temp_out = temperature_raw .strip ().partition ("%" )
98+ temp_in , _ , humi_in = in_data .strip ().partition ("/" )
99+ mode_raw = cast (PageElement , cast (Tag , container .find_all (class_ = "col-12" )[3 ]).find ("span" )).text
100+ co2_raw = cast (PageElement , cast (Tag , container .find_all (class_ = "col-12" )[4 ]).find ("b" )).text
101+ filter_raw = (
102+ cast (
103+ str ,
104+ cast (Tag , cast (Tag , container .find_all (class_ = "filterBox" )[1 ]).div )["style" ],
105+ )
106+ .partition (":" )[2 ]
107+ .partition ("%" )[0 ]
108+ )
109+ fan_raw = (
110+ cast (
111+ str ,
112+ cast (Tag , cast (Tag , container .find_all (class_ = "filterBox" )[2 ]).div )["style" ],
113+ )
114+ .partition (":" )[2 ]
115+ .partition ("%" )[0 ]
116+ )
117+ light_raw = cast (str , cast (Tag , container .find (id = "myRange" ))["value" ])
107118
108119 return Status (
109120 device = device ,
110- name = cast (Tag , content .find (class_ = ' deviceName' )).text ,
121+ name = cast (Tag , content .find (class_ = " deviceName" )).text ,
111122 temperature_in = int (_strip_unit (temp_in )),
112123 humidity_in = int (_strip_unit (humi_in )),
113124 temperature_out = int (_strip_unit (temp_out )),
@@ -122,11 +133,11 @@ async def get_status(client: httpx.AsyncClient, device: str) -> Status:
122133 raise RecuairError (f"Invalid response returned from device { device } " ) from error
123134
124135
125- async def post_request (client : httpx .AsyncClient , device : str , data : Dict [str , Any ]) -> None :
136+ async def post_request (client : httpx .AsyncClient , device : str , data : dict [str , Any ]) -> None :
126137 """Send a POST request to the device."""
127138 try :
128139 # XXX: Disable redirects. Recuair returns 301 for POST requests.
129- response = await client .post (f' http://{ device } /' , data = data , timeout = 5 , follow_redirects = False )
140+ response = await client .post (f" http://{ device } /" , data = data , timeout = 5 , follow_redirects = False )
130141 except httpx .HTTPError as error :
131142 _LOGGER .debug ("Error encountered: %s" , error )
132143 raise RecuairError (f"Error from device { device } : { error } " ) from error
@@ -136,38 +147,43 @@ async def post_request(client: httpx.AsyncClient, device: str, data: Dict[str, A
136147 _LOGGER .debug ("Response [%s]: %s" , response , response .text )
137148
138149
139- X = TypeVar ('X' )
150+ X = TypeVar ("X" )
140151
141152
142153# XXX: Add retry, recuair devices are often irresponsive.
143154def _wrap_retry (func : Callable [..., X ]) -> Callable [..., X ]:
144155 return retry (reraise = True , stop = stop_after_attempt (10 ), wait = wait_exponential (max = 30 ))(func )
145156
146157
147- async def _run (options : Dict [str , str ]) -> None :
158+ async def _run (options : dict [str , str ]) -> None : # noqa: C901
148159 """Actually run the command."""
149160 error_found = False
150161 async with httpx .AsyncClient () as client :
151- coros : List [Coroutine ] = []
152- for device in options [' <device>' ]:
153- if options [' start' ]:
162+ coros : list [Coroutine ] = []
163+ for device in options [" <device>" ]:
164+ if options [" start" ]:
154165 # XXX: Start in auto mode. Recuair GUI starts on mode 1.
155- coros .append (_wrap_retry (post_request )(client , device , {' mode' : ' auto' }))
156- elif options [' stop' ]:
157- coros .append (_wrap_retry (post_request )(client , device , {' mode' : ' off' }))
158- elif options [' holiday' ]:
159- coros .append (_wrap_retry (post_request )(client , device , {' mode' : ' holiday' }))
160- elif options [' bypass' ]:
161- coros .append (_wrap_retry (post_request )(client , device , {' mode' : ' bypass' }))
162- elif options [' light' ]:
166+ coros .append (_wrap_retry (post_request )(client , device , {" mode" : " auto" }))
167+ elif options [" stop" ]:
168+ coros .append (_wrap_retry (post_request )(client , device , {" mode" : " off" }))
169+ elif options [" holiday" ]:
170+ coros .append (_wrap_retry (post_request )(client , device , {" mode" : " holiday" }))
171+ elif options [" bypass" ]:
172+ coros .append (_wrap_retry (post_request )(client , device , {" mode" : " bypass" }))
173+ elif options [" light" ]:
163174 # XXX: Recuair doesn't accept only change in light intensity.
164175 # Whole light setting has to be provided.
165- if options [' off' ]:
176+ if options [" off" ]:
166177 coros .append (
167- _wrap_retry (post_request )(client , device , {'r' : '0' , 'g' : '0' , 'b' : '0' , 'intensity' : '0' }))
178+ _wrap_retry (post_request )(client , device , {"r" : "0" , "g" : "0" , "b" : "0" , "intensity" : "0" })
179+ )
168180 else :
169- data = {'r' : options ['<red>' ], 'g' : options ['<green>' ], 'b' : options ['<blue>' ],
170- 'intensity' : options ['<intensity>' ]}
181+ data = {
182+ "r" : options ["<red>" ],
183+ "g" : options ["<green>" ],
184+ "b" : options ["<blue>" ],
185+ "intensity" : options ["<intensity>" ],
186+ }
171187 coros .append (_wrap_retry (post_request )(client , device , data ))
172188 else :
173189 coros .append (_wrap_retry (get_status )(client , device ))
@@ -184,16 +200,17 @@ async def _run(options: Dict[str, str]) -> None:
184200 sys .exit (1 )
185201
186202
187- def main (argv : Optional [List [str ]] = None ) -> None :
203+ def main (argv : Optional [list [str ]] = None ) -> None :
188204 """Run the CLI."""
189205 options = docopt (__doc__ , version = __version__ , argv = argv )
190206
191- if options ['--debug' ]: # pragma: no cover
192- logging .basicConfig (level = logging .DEBUG ,
193- format = '%(asctime)s %(levelname)-8s %(name)s:%(funcName)s: %(message)s' )
207+ if options ["--debug" ]: # pragma: no cover
208+ logging .basicConfig (
209+ level = logging .DEBUG , format = "%(asctime)s %(levelname)-8s %(name)s:%(funcName)s: %(message)s"
210+ )
194211
195212 asyncio .run (_run (options ))
196213
197214
198- if __name__ == ' __main__' : # pragma: no cover
215+ if __name__ == " __main__" : # pragma: no cover
199216 main ()
0 commit comments