diff --git a/custom_components/sengledapi/light.py b/custom_components/sengledapi/light.py index c450ce5..1ba0f3e 100755 --- a/custom_components/sengledapi/light.py +++ b/custom_components/sengledapi/light.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -"""Platform for light Sengled hintegration.""" +"""Platform for light Sengled integration.""" import logging from datetime import timedelta @@ -11,11 +11,8 @@ ATTR_COLOR_TEMP, ATTR_HS_COLOR, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, - SUPPORT_COLOR, - LightEntity, ColorMode, + LightEntity, ) from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.util import color as colorutil @@ -33,7 +30,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sengled Light platform.""" - _LOGGER.debug("""Creating new Sengled light component""") + _LOGGER.debug("Creating new Sengled light component") # Add devices add_entities( [ @@ -55,7 +52,7 @@ def __init__(self, light): self._name = light._friendly_name self._state = light._state self._brightness = light._brightness - self._avaliable = light._avaliable + self._available = light._available self._device_mac = light._device_mac self._device_model = light._device_model self._color_temperature = light._color_temperature @@ -83,42 +80,28 @@ def unique_id(self): @property def available(self): - """Return the connection status of this light""" - _LOGGER.debug("Light.py _avaliable %s", self._avaliable) - return self._avaliable + """Return the connection status of this light.""" + _LOGGER.debug("Light.py _available %s", self._available) + return self._available @property def extra_state_attributes(self): """Return device attributes of the entity.""" - if self._device_model == "E13-N11": - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - "state": self._state, - "available": self._avaliable, - "device model": self._device_model, - "rssi": self._device_rssi, - "mac": self._device_mac, - "alarm status ": self._alarm_status, - "color": self._color, - "color Temp": self._color_temperature, - "color r": self._rgb_color_r, - "color g": self._rgb_color_g, - "color b": self._rgb_color_b, - } - else: - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - "state": self._state, - "available": self._avaliable, - "device model": self._device_model, - "rssi": self._device_rssi, - "mac": self._device_mac, - "color": self._color, - "color Temp": self._color_temperature, - "color r": self._rgb_color_r, - "color g": self._rgb_color_g, - "color b": self._rgb_color_b, - } + attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + "state": self._state, + "available": self._available, + "device model": self._device_model, + "rssi": self._device_rssi, + "mac": self._device_mac, + "alarm status ": self._alarm_status, + "color": self._color, + "color Temp": self._color_temperature, + "color r": self._rgb_color_r, + "color g": self._rgb_color_g, + "color b": self._rgb_color_b, + } + return attributes @property def color_temp(self): @@ -154,38 +137,31 @@ def is_on(self): return self._state @property - def supported_features(self): - """Flags Supported Features""" - features = 0 + def supported_color_modes(self): + """Return the supported color modes for the light.""" + color_modes = set() if self._support_brightness: - features = SUPPORT_BRIGHTNESS + color_modes.add(ColorMode.BRIGHTNESS) if self._support_color_temp: - features = features | SUPPORT_COLOR_TEMP + color_modes.add(ColorMode.COLOR_TEMP) if self._support_color: - features = features | SUPPORT_COLOR - _LOGGER.debug("supported_features: %s", features) - return features + color_modes.add(ColorMode.HS) + return color_modes @property - def supported_color_modes(self): - """Flags Supported Features""" - features = set() - if self._support_brightness: - features.add(ColorMode.BRIGHTNESS) - if self._support_color_temp: - features.add(ColorMode.COLOR_TEMP) + def color_mode(self): + """Return the current color mode of the light.""" if self._support_color: - features.add(ColorMode.HS) - _LOGGER.debug("supported_color_modes: %s", features) - return features + return ColorMode.HS + elif self._support_color_temp: + return ColorMode.COLOR_TEMP + else: + return ColorMode.BRIGHTNESS async def async_turn_on(self, **kwargs): - _LOGGER.debug("turn_on kwargs: %s", kwargs) """Turn on or control the light.""" - if ( - ATTR_BRIGHTNESS not in kwargs - and ATTR_HS_COLOR not in kwargs - and ATTR_COLOR_TEMP not in kwargs + if not any( + key in kwargs for key in (ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP) ): await self._light.async_toggle(ON) if ATTR_BRIGHTNESS in kwargs: @@ -210,7 +186,7 @@ async def async_update(self): """ await self._light.async_update() self._state = self._light.is_on() - self._avaliable = self._light._avaliable + self._available = self._light._available self._state = self._light._state self._brightness = self._light._brightness self._color_temperature = self._light._color_temperature diff --git a/custom_components/sengledapi/sengledapi/devices/bulbs/bulb.py b/custom_components/sengledapi/sengledapi/devices/bulbs/bulb.py index 0d66baf..f189f9e 100755 --- a/custom_components/sengledapi/sengledapi/devices/bulbs/bulb.py +++ b/custom_components/sengledapi/sengledapi/devices/bulbs/bulb.py @@ -40,7 +40,7 @@ def __init__( self._device_mac = device_mac self._friendly_name = friendly_name self._state = state - self._avaliable = isonline + self._available = isonline self._just_changed_state = True self._device_model = device_model self._device_rssi = -30 @@ -67,8 +67,6 @@ async def async_toggle(self, onoff): if onoff == "1": self._state = True else: - # We don't know what is coming in this parameter and the API is sensitive to other values - onoff = "0" self._state = False if self._wifi_device: _LOGGER.info( @@ -89,7 +87,7 @@ async def async_toggle(self, onoff): ) else: _LOGGER.info( - "SengledApi: Bulb %s %s toggling.", + "SengledApi: Bulb %s %s turning on.", self._friendly_name, self._device_mac, ) diff --git a/custom_components/sengledapi/sengledapi/devices/exceptions.py b/custom_components/sengledapi/sengledapi/devices/exceptions.py index 83df6c3..58b93c5 100755 --- a/custom_components/sengledapi/sengledapi/devices/exceptions.py +++ b/custom_components/sengledapi/sengledapi/devices/exceptions.py @@ -19,5 +19,9 @@ class AccessTokenError(SengledApiError): pass -class SengledApiAccessToken: - pass +class SengledApiAccessToken(SengledApiError): + """Raised when the api encounters issues with the AccessToken.""" + + def __init__(self, message="Invalid or missing AccessToken"): + self.message = message + super().__init__(self.message) diff --git a/custom_components/sengledapi/sengledapi/devices/request.py b/custom_components/sengledapi/sengledapi/devices/request.py index a1b5dd9..27901cc 100755 --- a/custom_components/sengledapi/sengledapi/devices/request.py +++ b/custom_components/sengledapi/sengledapi/devices/request.py @@ -14,6 +14,17 @@ _LOGGER.info("SengledApi: Initializing Request") +import asyncio +import functools +from concurrent.futures import ThreadPoolExecutor + +async def async_create_ssl_context(): + loop = asyncio.get_running_loop() + with ThreadPoolExecutor() as executor: + return await loop.run_in_executor( + executor, functools.partial(ssl.create_default_context, cafile=certifi.where()) + ) + class Request: def __init__(self, url, payload, no_return=False): @@ -30,33 +41,26 @@ def __init__(self, url, payload, no_return=False): "Connection": "keep-alive", } - def get_response(self, jsession_id): - self._header = { - "Content-Type": "application/json", - "Cookie": "JSESSIONID={}".format(jsession_id), - "Connection": "keep-alive", - } - - r = requests.post(self._url, headers=self._header, data=self._payload) - data = r.json() - return data - async def async_get_response(self, jsession_id): self._header = { "Content-Type": "application/json", - "Cookie": "JSESSIONID={}".format(jsession_id), - "Host": "element.cloud.sengled.com:443", + "Cookie": f"JSESSIONID={jsession_id}", "Connection": "keep-alive", } + + # Asynchronously create the SSL context in a non-blocking way. + sslcontext = await async_create_ssl_context() + # Use aiohttp's ClientSession for asynchronous HTTP requests. async with aiohttp.ClientSession() as session: - sslcontext = ssl.create_default_context(cafile=certifi.where()) - async with session.post( - self._url, headers=self._header, data=self._payload, ssl=sslcontext - ) as resp: - data = await resp.json() - # _LOGGER.debug("SengledApi: data from Response %s ", str(data)) - return data + async with session.post(self._url, headers=self._header, data=self._payload, ssl=sslcontext) as response: + # Make sure to handle potential exceptions and non-JSON responses appropriately. + if response.status == 200: + data = await response.json() + return data + else: + _LOGGER.error("Failed to get response, status: %s", response.status) + return None ########################Login##################################### def get_login_response(self): @@ -68,14 +72,18 @@ def get_login_response(self): async def async_get_login_response(self): _LOGGER.info("SengledApi: Get Login Response async.") + sslcontext = await async_create_ssl_context() async with aiohttp.ClientSession() as session: - sslcontext = ssl.create_default_context(cafile=certifi.where()) async with session.post( self._url, headers=self._header, data=self._payload, ssl=sslcontext ) as resp: - data = await resp.json() - _LOGGER.debug("SengledApi: Get Login Response %s ", str(data)) - return data + if resp.status == 200: + data = await resp.json() + _LOGGER.debug("SengledApi: Get Login Response %s ", str(data)) + return data + else: + _LOGGER.error("Failed to get login response, status: %s", resp.status) + return None ######################Session Timeout################################# def is_session_timeout_response(self, jsession_id): @@ -100,13 +108,17 @@ async def async_is_session_timeout_response(self, jsession_id): "sid": jsession_id, "X-Requested-With": "com.sengled.life2", } + sslcontext = await async_create_ssl_context() async with aiohttp.ClientSession() as session: - sslcontext = ssl.create_default_context(cafile=certifi.where()) async with session.post( self._url, headers=self._header, data=self._payload, ssl=sslcontext ) as resp: - data = await resp.json() - _LOGGER.info( - "SengledApi: Get Session Timeout Response Async %s", str(data) - ) - return data + if resp.status == 200: + data = await resp.json() + _LOGGER.info( + "SengledApi: Get Session Timeout Response Async %s", str(data) + ) + return data + else: + _LOGGER.error("Failed to get session timeout response, status: %s", resp.status) + return None diff --git a/custom_components/sengledapi/sengledapi/sengledapi.py b/custom_components/sengledapi/sengledapi/sengledapi.py index a624e52..1f92393 100755 --- a/custom_components/sengledapi/sengledapi/sengledapi.py +++ b/custom_components/sengledapi/sengledapi/sengledapi.py @@ -62,7 +62,7 @@ async def async_login(self, username, password, device_id): _LOGGER.info("Sengledapi: Login") if SESSION.jsession_id: - if not self.async_is_session_timeout(): + if not await self.async_is_session_timeout(): return url = "https://ucenter.cloud.sengled.com/user/app/customer/v2/AuthenCross.json" @@ -95,7 +95,7 @@ async def async_login(self, username, password, device_id): return True def is_valid_login(self): - if SESSION.jsession_id == None: + if SESSION.jsession_id is None: return False return True @@ -148,7 +148,7 @@ async def async_get_server_info(self): SESSION.mqtt_server["host"] = url.netloc SESSION.mqtt_server["port"] = 443 SESSION.mqtt_server["path"] = url.path - _LOGGER.debug("SengledApi: Parese MQTT Server Info" + str(url)) + _LOGGER.debug("SengledApi: Parse MQTT Server Info" + str(url)) async def async_get_wifi_devices(self): """ @@ -228,7 +228,6 @@ async def discover_devices(self): async def async_list_switch(self): _LOGGER.info("Sengled Api listing switches.") switch = [] - # This is my room list for device in await self.async_get_devices(): _LOGGER.debug(device) if "lampInfos" in device: @@ -247,37 +246,35 @@ async def async_list_switch(self): ) return switch - #######################Do request####################################################### async def async_do_request(self, url, payload, jsessionId): try: return await Request(url, payload).async_get_response(jsessionId) - except: - return Request(url, payload).get_response(jsessionId) + except Exception as e: + _LOGGER.error("Error in async_do_request: %s", e) + raise - ###################################Login Request only############################### async def async_do_login_request(self, url, payload): _LOGGER.info("SengledApi: Login Request.") try: return await Request(url, payload).async_get_login_response() - except: + except Exception as e: + _LOGGER.error("Error in async_do_login_request: %s", e) return Request(url, payload).get_login_response() - ######################################Session Timeout####################################### async def async_do_is_session_timeout_request(self, url, payload): _LOGGER.info("SengledApi: Sengled Api doing request.") try: return await Request(url, payload).async_is_session_timeout_response( SESSION.jsession_id ) - except: + except Exception as e: + _LOGGER.error("Error in async_do_is_session_timeout_request: %s", e) return Request(url, payload).is_session_timeout_response( SESSION.jsession_id ) - #########################MQTT################################################# def initialize_mqtt(self): _LOGGER.info("SengledApi: Initialize the MQTT connection") - """Initialize the MQTT connection.""" if not SESSION.jsession_id: return False @@ -307,7 +304,6 @@ def on_message(api, userdata, msg): return True def reinitialize_mqtt(self): - """Re-initialize the MQTT connection.""" _LOGGER.info("SengledApi: Re-initialize the MQTT connection") if SESSION.mqtt_client is None or not SESSION.jsession_id: return False @@ -329,12 +325,6 @@ def reinitialize_mqtt(self): return True def publish_mqtt(self, topic, payload=None): - """ - Publish an MQTT message. - topic -- topic to publish the message on - payload -- message to send - Returns True if publish succeeded, False if not. - """ _LOGGER.info("SengledApi: Publish MQTT message") if SESSION.mqtt_client is None: return False @@ -343,19 +333,14 @@ def publish_mqtt(self, topic, payload=None): _LOGGER.debug("SengledApi: Publish Mqtt %s", str(r)) try: r.wait_for_publish() - return r.is_published() + return r.is_published except ValueError: pass return False def subscribe_mqtt(self, topic, callback): - _LOGGER.info("SengledApi: Subscribe to an MQTT Topic") - """ - Subscribe to an MQTT topic. - topic -- topic to subscribe to - callback -- callback to call when a message comes in - """ + _LOGGER.info("SengledApi: Subscribe to an MQTT Topic") if SESSION.mqtt_client is None: return False @@ -369,10 +354,5 @@ def subscribe_mqtt(self, topic, callback): def unsubscribe_mqtt(self, topic, callback): _LOGGER.info("SengledApi: Unsubscribe from an MQTT topic") - """ - Unsubscribe from an MQTT topic. - topic -- topic to unsubscribe from - callback -- callback from previous subscription - """ if topic in SESSION.subscribe: del SESSION.subscribe[topic]