diff --git a/custom_components/ac_infinity/ac_infinity.py b/custom_components/ac_infinity/ac_infinity.py index 5a6aff1..3bdf8e6 100644 --- a/custom_components/ac_infinity/ac_infinity.py +++ b/custom_components/ac_infinity/ac_infinity.py @@ -1,5 +1,5 @@ from datetime import timedelta -from typing import Any, List +from typing import Any from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import UpdateFailed @@ -7,42 +7,42 @@ from .client import ACInfinityClient from .const import ( - DEVICE_KEY_DEVICE_ID, - DEVICE_KEY_DEVICE_INFO, - DEVICE_KEY_DEVICE_NAME, - DEVICE_KEY_DEVICE_TYPE, - DEVICE_KEY_HW_VERSION, - DEVICE_KEY_MAC_ADDR, - DEVICE_KEY_PORTS, - DEVICE_KEY_SW_VERSION, - DEVICE_PORT_KEY_NAME, - DEVICE_PORT_KEY_PORT, DOMAIN, HOST, MANUFACTURER, + PROPERTY_KEY_DEVICE_ID, + PROPERTY_KEY_DEVICE_INFO, + PROPERTY_KEY_DEVICE_NAME, + PROPERTY_KEY_DEVICE_TYPE, + PROPERTY_KEY_HW_VERSION, + PROPERTY_KEY_MAC_ADDR, + PROPERTY_KEY_PORTS, + PROPERTY_KEY_SW_VERSION, + PROPERTY_PORT_KEY_NAME, + PROPERTY_PORT_KEY_PORT, ) class ACInfinityDevice: def __init__(self, device_json) -> None: # device info - self._device_id = str(device_json[DEVICE_KEY_DEVICE_ID]) - self._mac_addr = device_json[DEVICE_KEY_MAC_ADDR] - self._device_name = device_json[DEVICE_KEY_DEVICE_NAME] - + self._device_id = str(device_json[PROPERTY_KEY_DEVICE_ID]) + self._mac_addr = device_json[PROPERTY_KEY_MAC_ADDR] + self._device_name = device_json[PROPERTY_KEY_DEVICE_NAME] + self._identifier = (DOMAIN, self._device_id) self._ports = [ - ACInfinityDevicePort(port) - for port in device_json[DEVICE_KEY_DEVICE_INFO][DEVICE_KEY_PORTS] + ACInfinityDevicePort(self, port) + for port in device_json[PROPERTY_KEY_DEVICE_INFO][PROPERTY_KEY_PORTS] ] self._device_info = DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, + identifiers={self._identifier}, name=self._device_name, manufacturer=MANUFACTURER, - hw_version=device_json[DEVICE_KEY_HW_VERSION], - sw_version=device_json[DEVICE_KEY_SW_VERSION], + hw_version=device_json[PROPERTY_KEY_HW_VERSION], + sw_version=device_json[PROPERTY_KEY_SW_VERSION], model=self.__get_device_model_by_device_type( - device_json[DEVICE_KEY_DEVICE_TYPE] + device_json[PROPERTY_KEY_DEVICE_TYPE] ), ) @@ -66,18 +66,30 @@ def ports(self): def device_info(self): return self._device_info + @property + def identifier(self): + return self._identifier + def __get_device_model_by_device_type(self, device_type: int): match device_type: case 11: - return "Controller 69 Pro (CTR69P)" + return "UIS Controller 69 Pro (CTR69P)" case _: - return f"Controller Type {device_type}" + return f"UIS Controller Type {device_type}" class ACInfinityDevicePort: - def __init__(self, device_json) -> None: - self._port_id = device_json[DEVICE_PORT_KEY_PORT] - self._port_name = device_json[DEVICE_PORT_KEY_NAME] + def __init__(self, device: ACInfinityDevice, device_port_json) -> None: + self._port_id = device_port_json[PROPERTY_PORT_KEY_PORT] + self._port_name = device_port_json[PROPERTY_PORT_KEY_NAME] + + self._device_info = DeviceInfo( + identifiers={(DOMAIN, f"{device._device_id}_{self._port_id}")}, + name=f"{device._device_name} {self._port_name}", + manufacturer=MANUFACTURER, + via_device=device.identifier, + model="UIS Enabled Device", + ) @property def port_id(self): @@ -87,66 +99,102 @@ def port_id(self): def port_name(self): return self._port_name + @property + def device_info(self): + return self._device_info + class ACInfinity: MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) def __init__(self, email: str, password: str) -> None: self._client = ACInfinityClient(HOST, email, password) - self._data: list[dict[str, Any]] = [] + self._devices: dict[str, dict[str, Any]] = {} + self._port_settings: dict[str, dict[int, Any]] = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) async def update(self): + """refreshes the values of properties and settings from the AC infinity API""" try: if not self._client.is_logged_in(): await self._client.login() - self._data = await self._client.get_all_device_info() + device_list = await self._client.get_all_device_info() + for device in device_list: + device_id = device[PROPERTY_KEY_DEVICE_ID] + self._devices[device_id] = device + self._port_settings[device_id] = {} + for port in device[PROPERTY_KEY_DEVICE_INFO][PROPERTY_KEY_PORTS]: + port_id = port[PROPERTY_PORT_KEY_PORT] + self._port_settings[device_id][ + port_id + ] = await self._client.get_device_port_settings(device_id, port_id) + except Exception: raise UpdateFailed from Exception - def get_all_device_meta_data(self) -> List[ACInfinityDevice]: + def get_all_device_meta_data(self) -> list[ACInfinityDevice]: """gets device metadata, such as ids, labels, macaddr, etc.. that are not expected to change""" - if (self._data) is None: + if (self._devices) is None: return [] - return [ACInfinityDevice(device) for device in self._data] - - def get_device_property(self, device_id: (str | int), property: str): - """gets a property, if it exists, from a given device, if it exists""" - result = next( - ( - device - for device in self._data - if device[DEVICE_KEY_DEVICE_ID] == str(device_id) - ), - None, - ) + return [ACInfinityDevice(device) for device in self._devices.values()] - if result is not None: - if property in result: - return result[property] - elif property in result[DEVICE_KEY_DEVICE_INFO]: - return result[DEVICE_KEY_DEVICE_INFO][property] + def get_device_property(self, device_id: (str | int), property_key: str): + """gets a property of a controller, if it exists, from a given device, if it exists""" + if str(device_id) in self._devices: + result = self._devices[str(device_id)] + if property_key in result: + return result[property_key] + elif property_key in result[PROPERTY_KEY_DEVICE_INFO]: + return result[PROPERTY_KEY_DEVICE_INFO][property_key] return None def get_device_port_property( - self, device_id: (str | int), port_id: int, property: str + self, device_id: (str | int), port_id: int, property_key: str ): - """gets a property, if it exists, from the given port, if it exists, from the given device, if it exists""" - result = next( - ( - port - for device in self._data - if device[DEVICE_KEY_DEVICE_ID] == str(device_id) - for port in device[DEVICE_KEY_DEVICE_INFO][DEVICE_KEY_PORTS] - if port[DEVICE_PORT_KEY_PORT] == port_id - ), - None, - ) + """gets a property, if it exists, from the given port, if it exists, from a child of the given controller device, if it exists + + Properties are read-only values reported from the parent controller via devInfoListAll, as opposed to settings with are read/write + values reported from getdevModeSettingList for the individual port device + """ + if str(device_id) in self._devices: + device = self._devices[str(device_id)] + result = next( + ( + port + for port in device[PROPERTY_KEY_DEVICE_INFO][PROPERTY_KEY_PORTS] + if port[PROPERTY_PORT_KEY_PORT] == port_id + ), + None, + ) + + if result is not None and property_key in result: + return result[property_key] - if result is not None and property in result: - return result[property] + return None + + def get_device_port_setting( + self, device_id: (str | int), port_id: int, setting: str + ): + """gets the current set value for a given device setting + + Settings are read/write values reported from getdevModeSettinList for an individual port device, as opposed to + port properties, which are read-only values reported by the parent controller via devInfoListAll + """ + device_id_str = str(device_id) + if ( + device_id_str in self._port_settings + and port_id in self._port_settings[device_id_str] + and setting in self._port_settings[device_id_str][port_id] + ): + return self._port_settings[device_id_str][port_id][setting] return None + + async def set_device_port_setting( + self, device_id: (str | int), port_id: int, setting: str, value: int + ): + """set a desired value for a given device setting""" + await self._client.set_device_port_setting(device_id, port_id, setting, value) diff --git a/custom_components/ac_infinity/binary_sensor.py b/custom_components/ac_infinity/binary_sensor.py index 9a6422c..2219eda 100644 --- a/custom_components/ac_infinity/binary_sensor.py +++ b/custom_components/ac_infinity/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from custom_components.ac_infinity.const import DEVICE_PORT_KEY_ONLINE, DOMAIN +from custom_components.ac_infinity.const import DOMAIN, SENSOR_PORT_KEY_ONLINE from .ac_infinity import ACInfinity, ACInfinityDevice, ACInfinityDevicePort from .utilities import get_device_port_property_name, get_device_port_property_unique_id @@ -29,7 +29,7 @@ def __init__( self._property_key = property_key self._attr_icon = icon - self._attr_device_info = device.device_info + self._attr_device_info = port.device_info self._attr_device_class = device_class self._attr_unique_id = get_device_port_property_unique_id( device, port, property_key @@ -51,7 +51,7 @@ async def async_setup_entry( acis: ACInfinity = hass.data[DOMAIN][config.entry_id] device_sensors = { - DEVICE_PORT_KEY_ONLINE: { + SENSOR_PORT_KEY_ONLINE: { "label": "Status", "deviceClass": BinarySensorDeviceClass.PLUG, "icon": "mdi:power", diff --git a/custom_components/ac_infinity/client.py b/custom_components/ac_infinity/client.py index 70eb25d..1dc0582 100644 --- a/custom_components/ac_infinity/client.py +++ b/custom_components/ac_infinity/client.py @@ -4,6 +4,8 @@ API_URL_LOGIN = "/api/user/appUserLogin" API_URL_GET_DEVICE_INFO_LIST_ALL = "/api/user/devInfoListAll" +API_URL_GET_DEV_MODE_SETTING = "/api/dev/getdevModeSettingList" +API_URL_ADD_DEV_MODE = "/api/dev/addDevMode" class ACInfinityClient: @@ -28,7 +30,7 @@ def is_logged_in(self): async def get_all_device_info(self): if not self.is_logged_in(): - raise ACInfinityClientCannotConnect("Aerogarden client is not logged in.") + raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") headers = self.__create_headers(use_auth_token=True) json = await self.__post( @@ -36,6 +38,68 @@ async def get_all_device_info(self): ) return json["data"] + async def get_device_port_settings(self, device_id: (str | int), port_id: int): + if not self.is_logged_in(): + raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") + + headers = self.__create_headers(use_auth_token=True) + json = await self.__post( + API_URL_GET_DEV_MODE_SETTING, {"devId": device_id, "port": port_id}, headers + ) + return json["data"] + + async def set_device_port_setting( + self, device_id: (str | int), port_id: int, setting: str, value: int + ): + active_settings = await self.get_device_port_settings(device_id, port_id) + payload = { + "acitveTimerOff": active_settings["acitveTimerOff"], + "acitveTimerOn": active_settings["acitveTimerOn"], + "activeCycleOff": active_settings["activeCycleOff"], + "activeCycleOn": active_settings["activeCycleOn"], + "activeHh": active_settings["activeHh"], + "activeHt": active_settings["activeHt"], + "activeHtVpd": active_settings["activeHtVpd"], + "activeHtVpdNums": active_settings["activeHtVpdNums"], + "activeLh": active_settings["activeLh"], + "activeLt": active_settings["activeLt"], + "activeLtVpd": active_settings["activeLtVpd"], + "activeLtVpdNums": active_settings["activeLtVpdNums"], + "atType": active_settings["atType"], + "devHh": active_settings["devHh"], + "devHt": active_settings["devHt"], + "devHtf": active_settings["devHtf"], + "devId": active_settings["devId"], + "devLh": active_settings["devLh"], + "devLt": active_settings["devLt"], + "devLtf": active_settings["devLtf"], + "externalPort": active_settings["externalPort"], + "hTrend": active_settings["hTrend"], + "isOpenAutomation": active_settings["isOpenAutomation"], + "onSpead": active_settings["onSpead"], + "offSpead": active_settings["offSpead"], + "onlyUpdateSpeed": active_settings["onlyUpdateSpeed"], + "schedEndtTime": active_settings["schedEndtTime"], + "schedStartTime": active_settings["schedStartTime"], + "settingMode": active_settings["settingMode"], + "surplus": active_settings["surplus"] or 0, + "tTrend": active_settings["tTrend"], + "targetHumi": active_settings["targetHumi"], + "targetHumiSwitch": active_settings["targetHumiSwitch"], + "targetTSwitch": active_settings["targetTSwitch"], + "targetTemp": active_settings["targetTemp"], + "targetTempF": active_settings["targetTempF"], + "targetVpd": active_settings["targetVpd"], + "targetVpdSwitch": active_settings["targetVpdSwitch"], + "trend": active_settings["trend"], + "unit": active_settings["unit"], + "vpdSettingMode": active_settings["vpdSettingMode"], + } + + payload[setting] = int(value) + headers = self.__create_headers(use_auth_token=True) + _ = await self.__post(API_URL_ADD_DEV_MODE, payload, headers) + async def __post(self, path, post_data, headers): async with async_timeout.timeout(10), aiohttp.ClientSession( raise_for_status=False, headers=headers @@ -45,7 +109,10 @@ async def __post(self, path, post_data, headers): json = await response.json() if json["code"] != 200: - raise ACInfinityClientInvalidAuth + if path == API_URL_LOGIN: + raise ACInfinityClientInvalidAuth + else: + raise ACInfinityClientRequestFailed(json) return json @@ -67,3 +134,7 @@ class ACInfinityClientCannotConnect(HomeAssistantError): class ACInfinityClientInvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class ACInfinityClientRequestFailed(HomeAssistantError): + """Error to indicate a request failed""" diff --git a/custom_components/ac_infinity/const.py b/custom_components/ac_infinity/const.py index 3fe9301..8101417 100644 --- a/custom_components/ac_infinity/const.py +++ b/custom_components/ac_infinity/const.py @@ -4,28 +4,30 @@ MANUFACTURER = "AC Infinity" DOMAIN = "ac_infinity" -PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR] +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.SELECT, Platform.NUMBER] HOST = "http://www.acinfinityserver.com" -# Device Metadata -DEVICE_KEY_DEVICE_ID = "devId" -DEVICE_KEY_DEVICE_NAME = "devName" -DEVICE_KEY_MAC_ADDR = "devMacAddr" -DEVICE_KEY_DEVICE_INFO = "deviceInfo" -DEVICE_KEY_PORTS = "ports" -DEVICE_KEY_HW_VERSION = "hardwareVersion" -DEVICE_KEY_SW_VERSION = "firmwareVersion" -DEVICE_KEY_DEVICE_TYPE = "devType" +# devInfoListAll ReadOnly Device Fields +PROPERTY_KEY_DEVICE_ID = "devId" +PROPERTY_KEY_DEVICE_NAME = "devName" +PROPERTY_KEY_MAC_ADDR = "devMacAddr" +PROPERTY_KEY_DEVICE_INFO = "deviceInfo" +PROPERTY_KEY_PORTS = "ports" +PROPERTY_KEY_HW_VERSION = "hardwareVersion" +PROPERTY_KEY_SW_VERSION = "firmwareVersion" +PROPERTY_KEY_DEVICE_TYPE = "devType" +PROPERTY_PORT_KEY_PORT = "port" +PROPERTY_PORT_KEY_NAME = "portName" -# Device Port Metadata -DEVICE_PORT_KEY_PORT = "port" -DEVICE_PORT_KEY_NAME = "portName" +# devInfoListAll Sensor State Fields +SENSOR_KEY_TEMPERATURE = "temperature" +SENSOR_KEY_HUMIDITY = "humidity" +SENSOR_KEY_VPD = "vpdnums" +SENSOR_PORT_KEY_SPEAK = "speak" +SENSOR_PORT_KEY_ONLINE = "online" -# Device Sensor Fields -DEVICE_KEY_TEMPERATURE = "temperature" -DEVICE_KEY_HUMIDITY = "humidity" -DEVICE_KEY_VAPOR_PRESSURE_DEFICIT = "vpdnums" - -# Device Port Sensor Fields -DEVICE_PORT_KEY_SPEAK = "speak" -DEVICE_PORT_KEY_ONLINE = "online" +# getdevModeSettingList Setting Fields +SETTING_KEY_ON_SPEED = "onSpead" +SETTING_KEY_OFF_SPEED = "offSpead" +SETTING_KEY_AT_TYPE = "atType" +SETTING_KEY_SURPLUS = "surplus" diff --git a/custom_components/ac_infinity/number.py b/custom_components/ac_infinity/number.py index 66f820d..4e20acc 100644 --- a/custom_components/ac_infinity/number.py +++ b/custom_components/ac_infinity/number.py @@ -4,8 +4,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from custom_components.ac_infinity.const import ( - DEVICE_PORT_KEY_SPEAK, DOMAIN, + SETTING_KEY_OFF_SPEED, + SETTING_KEY_ON_SPEED, ) from .ac_infinity import ACInfinity, ACInfinityDevice, ACInfinityDevicePort @@ -18,7 +19,7 @@ def __init__( acis: ACInfinity, device: ACInfinityDevice, port: ACInfinityDevicePort, - property_key: str, + setting_key: str, sensor_label: str, device_class: str, min_value: int, @@ -27,23 +28,29 @@ def __init__( self._acis = acis self._device = device self._port = port - self._property_key = property_key + self._setting_key = setting_key self._attr_native_min_value = min_value self._attr_native_max_value = max_value - self._attr_device_info = device.device_info + self._attr_device_info = port.device_info self._attr_device_class = device_class self._attr_unique_id = get_device_port_property_unique_id( - device, port, property_key + device, port, setting_key ) self._attr_name = get_device_port_property_name(device, port, sensor_label) async def async_update(self) -> None: await self._acis.update() - self._attr_native_value = self._acis.get_device_port_property( - self._device.device_id, self._port.port_id, self._property_key + self._attr_native_value = self._acis.get_device_port_setting( + self._device.device_id, self._port.port_id, self._setting_key ) + async def async_set_native_value(self, value: int) -> None: + await self._acis.set_device_port_setting( + self._device.device_id, self._port.port_id, self._setting_key, value + ) + self._attr_native_value = value + async def async_setup_entry( hass: HomeAssistant, config: ConfigEntry, add_entities_callback: AddEntitiesCallback @@ -52,9 +59,15 @@ async def async_setup_entry( acis: ACInfinity = hass.data[DOMAIN][config.entry_id] - device_sensors = { - DEVICE_PORT_KEY_SPEAK: { - "label": "Intensity", + port_sesnors = { + SETTING_KEY_ON_SPEED: { + "label": "On Speed", + "deviceClass": NumberDeviceClass.POWER_FACTOR, + "min": 0, + "max": 10, + }, + SETTING_KEY_OFF_SPEED: { + "label": "Off Speed", "deviceClass": NumberDeviceClass.POWER_FACTOR, "min": 0, "max": 10, @@ -67,7 +80,7 @@ async def async_setup_entry( sensor_objects = [] for device in devices: for port in device.ports: - for key, descr in device_sensors.items(): + for key, descr in port_sesnors.items(): sensor_objects.append( ACInfinityPortNumberEntity( acis, diff --git a/custom_components/ac_infinity/select.py b/custom_components/ac_infinity/select.py new file mode 100644 index 0000000..cb99e0d --- /dev/null +++ b/custom_components/ac_infinity/select.py @@ -0,0 +1,96 @@ +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from custom_components.ac_infinity.ac_infinity import ( + ACInfinity, + ACInfinityDevice, + ACInfinityDevicePort, +) +from custom_components.ac_infinity.const import DOMAIN, SETTING_KEY_AT_TYPE + +from .utilities import get_device_port_property_name, get_device_port_property_unique_id + + +class ACInfinityPortSelectEntity(SelectEntity): + def __init__( + self, + acis: ACInfinity, + device: ACInfinityDevice, + port: ACInfinityDevicePort, + setting_key: str, + label: str, + options: list[str], + ) -> None: + self._acis = acis + self._device = device + self._port = port + self._setting_key = setting_key + + self._attr_device_info = port.device_info + self._attr_unique_id = get_device_port_property_unique_id( + device, port, setting_key + ) + self._attr_name = get_device_port_property_name(device, port, label) + self._attr_options = options + self._attr_current_option = options[0] + + async def async_update(self) -> None: + await self._acis.update() + option = self._acis.get_device_port_setting( + self._device.device_id, self._port.port_id, self._setting_key + ) + self._attr_current_option = self._attr_options[option - 1] # 1 to 0 based array + + async def async_select_option(self, option: str) -> None: + index = self._attr_options.index(option) + + await self._acis.set_device_port_setting( + self._device.device_id, self._port.port_id, self._setting_key, index + 1 + ) # 0 to 1 based array + self._attr_current_option = option + + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, add_entities_callback: AddEntitiesCallback +) -> None: + """Setup the AC Infinity Platform.""" + + acis: ACInfinity = hass.data[DOMAIN][config.entry_id] + + select_entities = { + SETTING_KEY_AT_TYPE: { + "label": "Mode", + "options": [ + "Off", + "On", + "Auto", + "Timer to On", + "Timer to Off", + "Cycle", + "Schedule", + "VPD", + ], + } + } + + await acis.update() + devices = acis.get_all_device_meta_data() + + sensor_objects = [] + for device in devices: + for port in device.ports: + for key, descr in select_entities.items(): + sensor_objects.append( + ACInfinityPortSelectEntity( + acis, + device, + port, + key, + str(descr["label"]), + list[str](descr["options"]), + ) + ) + + add_entities_callback(sensor_objects) diff --git a/custom_components/ac_infinity/sensor.py b/custom_components/ac_infinity/sensor.py index 8425de7..b1d2daa 100644 --- a/custom_components/ac_infinity/sensor.py +++ b/custom_components/ac_infinity/sensor.py @@ -8,11 +8,11 @@ from .ac_infinity import ACInfinity, ACInfinityDevice, ACInfinityDevicePort from .const import ( - DEVICE_KEY_HUMIDITY, - DEVICE_KEY_TEMPERATURE, - DEVICE_KEY_VAPOR_PRESSURE_DEFICIT, - DEVICE_PORT_KEY_SPEAK, DOMAIN, + SENSOR_KEY_HUMIDITY, + SENSOR_KEY_TEMPERATURE, + SENSOR_KEY_VPD, + SENSOR_PORT_KEY_SPEAK, ) from .utilities import ( get_device_port_property_name, @@ -71,7 +71,7 @@ def __init__( self._port = port self._property_key = property_key - self._attr_device_info = device.device_info + self._attr_device_info = port.device_info self._attr_device_class = device_class self._attr_native_unit_of_measurement = unit self._attr_unique_id = get_device_port_property_unique_id( @@ -95,19 +95,19 @@ async def async_setup_entry( acis: ACInfinity = hass.data[DOMAIN][config.entry_id] device_sensors = { - DEVICE_KEY_TEMPERATURE: { + SENSOR_KEY_TEMPERATURE: { "label": "Temperature", "deviceClass": SensorDeviceClass.TEMPERATURE, "unit": UnitOfTemperature.CELSIUS, "icon": None, # default }, - DEVICE_KEY_HUMIDITY: { + SENSOR_KEY_HUMIDITY: { "label": "Humidity", "deviceClass": SensorDeviceClass.HUMIDITY, "unit": PERCENTAGE, "icon": None, # default }, - DEVICE_KEY_VAPOR_PRESSURE_DEFICIT: { + SENSOR_KEY_VPD: { "label": "VPD", "deviceClass": SensorDeviceClass.PRESSURE, "unit": UnitOfPressure.KPA, @@ -116,8 +116,8 @@ async def async_setup_entry( } port_sensors = { - DEVICE_PORT_KEY_SPEAK: { - "label": "Power", + SENSOR_PORT_KEY_SPEAK: { + "label": "Current Speed", "deviceClass": SensorDeviceClass.POWER_FACTOR, "unit": None, "icon": "mdi:speedometer", diff --git a/tests/data_models.py b/tests/data_models.py index f2288d7..a0809a0 100644 --- a/tests/data_models.py +++ b/tests/data_models.py @@ -26,152 +26,303 @@ }, } -DEVICE_INFO_LIST_ALL = [ - { - "devId": str(DEVICE_ID), - "devCode": "ABCDEFG", - "devName": DEVICE_NAME, - "devType": 11, - "devAccesstime": 1692328784, - "devPortCount": 4, - "devOfftime": 1692328718, - "devMacAddr": MAC_ADDR, - "devVersion": 7, +DEVICE_INFO = { + "devId": str(DEVICE_ID), + "devCode": "ABCDEFG", + "devName": DEVICE_NAME, + "devType": 11, + "devAccesstime": 1692328784, + "devPortCount": 4, + "devOfftime": 1692328718, + "devMacAddr": MAC_ADDR, + "devVersion": 7, + "online": 1, + "isShare": 0, + "devExternalList": None, + "deviceInfo": { + "devId": DEVICE_ID, + "temperature": 2417, + "temperatureF": 7551, + "humidity": 7200, + "tTrend": 0, + "hTrend": 0, + "unit": 0, + "speak": 0, + "trend": 0, + "curMode": 3, + "remainTime": None, + "modeTye": 15, + "advTriggerInfo": None, + "notificationTrigger": None, + "alertTrigger": None, "online": 1, - "isShare": 0, - "devExternalList": None, - "deviceInfo": { - "devId": DEVICE_ID, - "temperature": 2417, - "temperatureF": 7551, - "humidity": 7200, - "tTrend": 0, - "hTrend": 0, - "unit": 0, - "speak": 0, - "trend": 0, - "curMode": 3, - "remainTime": None, - "modeTye": 15, - "advTriggerInfo": None, - "notificationTrigger": None, - "alertTrigger": None, - "online": 1, - "lkType": None, - "endTime": 1692328718, - "master": 0, - "masterPort": 2, - "allPortStatus": 7, - "ports": [ - { - "speak": 5, - "deviceType": None, - "trend": 0, - "port": 1, - "curMode": 7, - "remainTime": 46545, - "modeTye": 0, - "online": 1, - "portName": "Grow Lights", - "portAccess": None, - "portResistance": 3300, - "isOpenAutomation": 0, - "advUpdateTime": None, - "loadType": 0, - "loadState": 1, - "abnormalState": 0, - "overcurrentStatus": 0, - }, - { - "speak": 7, - "deviceType": None, - "trend": 0, - "port": 2, - "curMode": 2, - "remainTime": None, - "modeTye": 15, - "online": 1, - "portName": "Exhaust Fan", - "portAccess": None, - "portResistance": 5100, - "isOpenAutomation": 0, - "advUpdateTime": None, - "loadType": 0, - "loadState": 1, - "abnormalState": 0, - "overcurrentStatus": 0, - }, - { - "speak": 5, - "deviceType": None, - "trend": 0, - "port": 3, - "curMode": 2, - "remainTime": None, - "modeTye": 15, - "online": 1, - "portName": "Circulating Fan", - "portAccess": None, - "portResistance": 10000, - "isOpenAutomation": 0, - "advUpdateTime": None, - "loadType": 0, - "loadState": 1, - "abnormalState": 0, - "overcurrentStatus": 0, - }, - { - "speak": 0, - "deviceType": None, - "trend": 0, - "port": 4, - "curMode": 2, - "remainTime": None, - "modeTye": 15, - "online": 0, - "portName": "Port 4", - "portAccess": None, - "portResistance": 65535, - "isOpenAutomation": 0, - "advUpdateTime": None, - "loadType": 0, - "loadState": 0, - "abnormalState": 0, - "overcurrentStatus": 0, - }, - ], - "logCreateTime": None, - "isOpenAutomation": 0, - "advUpdateTime": None, - "loadState": 0, - "abnormalState": 0, - "deviceInfoI": None, - "tempCompare": 0, - "humiCompare": 0, - "ectdsType": None, - "tdsUnit": None, - "ecUnit": None, - "sensorCount": None, - "sensors": None, - "overcurrentStatus": 0, - "vpdnums": 83, - "vpdstatus": 0, - }, - "appEmail": EMAIL, - "devTimeZone": "GMT+00:00", - "createTime": None, - "timeGMT": None, - "timeZone": None, - "firmwareVersion": "3.2.25", - "hardwareVersion": "1.1", - "workMode": 1, - "zoneId": "America/Chicago", - "wifiName": None, - } -] + "lkType": None, + "endTime": 1692328718, + "master": 0, + "masterPort": 2, + "allPortStatus": 7, + "ports": [ + { + "speak": 5, + "deviceType": None, + "trend": 0, + "port": 1, + "curMode": 7, + "remainTime": 46545, + "modeTye": 0, + "online": 1, + "portName": "Grow Lights", + "portAccess": None, + "portResistance": 3300, + "isOpenAutomation": 0, + "advUpdateTime": None, + "loadType": 0, + "loadState": 1, + "abnormalState": 0, + "overcurrentStatus": 0, + }, + { + "speak": 7, + "deviceType": None, + "trend": 0, + "port": 2, + "curMode": 2, + "remainTime": None, + "modeTye": 15, + "online": 1, + "portName": "Exhaust Fan", + "portAccess": None, + "portResistance": 5100, + "isOpenAutomation": 0, + "advUpdateTime": None, + "loadType": 0, + "loadState": 1, + "abnormalState": 0, + "overcurrentStatus": 0, + }, + { + "speak": 5, + "deviceType": None, + "trend": 0, + "port": 3, + "curMode": 2, + "remainTime": None, + "modeTye": 15, + "online": 1, + "portName": "Circulating Fan", + "portAccess": None, + "portResistance": 10000, + "isOpenAutomation": 0, + "advUpdateTime": None, + "loadType": 0, + "loadState": 1, + "abnormalState": 0, + "overcurrentStatus": 0, + }, + { + "speak": 0, + "deviceType": None, + "trend": 0, + "port": 4, + "curMode": 2, + "remainTime": None, + "modeTye": 15, + "online": 0, + "portName": "Port 4", + "portAccess": None, + "portResistance": 65535, + "isOpenAutomation": 0, + "advUpdateTime": None, + "loadType": 0, + "loadState": 0, + "abnormalState": 0, + "overcurrentStatus": 0, + }, + ], + "logCreateTime": None, + "isOpenAutomation": 0, + "advUpdateTime": None, + "loadState": 0, + "abnormalState": 0, + "deviceInfoI": None, + "tempCompare": 0, + "humiCompare": 0, + "ectdsType": None, + "tdsUnit": None, + "ecUnit": None, + "sensorCount": None, + "sensors": None, + "overcurrentStatus": 0, + "vpdnums": 83, + "vpdstatus": 0, + }, + "appEmail": EMAIL, + "devTimeZone": "GMT+00:00", + "createTime": None, + "timeGMT": None, + "timeZone": None, + "firmwareVersion": "3.2.25", + "hardwareVersion": "1.1", + "workMode": 1, + "zoneId": "America/Chicago", + "wifiName": None, +} + +DEVICE_SETTING = { + "modeSetid": "1678871847944916993", + "devId": "1424979258063355749", + "externalPort": 4, + "offSpead": 0, + "onSpead": 5, + "activeHt": 1, + "devHt": 89, + "devHtf": 193, + "devLtf": 32, + "activeLt": 0, + "devLt": 0, + "activeHh": 0, + "devHh": 100, + "activeLh": 0, + "devLh": 0, + "acitveTimerOn": 0, + "acitveTimerOff": 0, + "activeCycleOn": 0, + "activeCycleOff": 0, + "schedStartTime": 65535, + "schedEndtTime": 65535, + "surplus": None, + "modeType": 15, + "activeHtVpd": 0, + "activeLtVpd": 0, + "activeHtVpdNums": 99, + "activeLtVpdNums": 1, + "targetTSwitch": 0, + "targetHumiSwitch": 0, + "settingMode": 0, + "vpdSettingMode": 0, + "targetVpdSwitch": 0, + "targetVpd": 0, + "targetTemp": 0, + "targetTempF": 32, + "targetHumi": 0, + "isUpdateVpdNums": False, + "co2TargetSwitch": None, + "co2SettingMode": None, + "co2HighSwitch": None, + "co2LowSwitch": None, + "co2HighValue": None, + "co2LowValue": None, + "co2TargetValue": None, + "co2FanTargetSwitch": None, + "co2FanSettingMode": None, + "co2FanHighSwitch": None, + "co2FanLowSwitch": None, + "co2FanHighValue": None, + "co2FanLowValue": None, + "co2FanTargetValue": None, + "moistureTargetSwitch": None, + "moistureSettingMode": None, + "moistureHighSwitch": None, + "moistureLowSwitch": None, + "moistureHighValue": None, + "moistureLowValue": None, + "moistureTargetValue": None, + "waterTempTargetSwitch": None, + "waterTempSettingMode": None, + "waterTempHighSwitch": None, + "waterTempLowSwitch": None, + "waterTempHighValueF": None, + "waterTempHighValue": None, + "waterTempLowValueF": None, + "waterTempLowValue": None, + "waterTempTargetValueF": None, + "waterTempTargetValue": None, + "phTargetSwitch": None, + "phSettingMode": None, + "phHighSwitch": None, + "phLowSwitch": None, + "phHighValue": None, + "phLowValue": None, + "phTargetValue": None, + "ecTdsTargetSwitch": None, + "ecTdsSettingMode": None, + "ecTdsHighSwitch": None, + "ecTdsLowSwitch": None, + "ecTdsHighValueEc": None, + "ecTdsHighValueTds": None, + "ecTdsLowValueEc": None, + "ecTdsLowValueTds": None, + "ecTdsTargetValueEc": None, + "ecTdsTargetValueTds": None, + "humidity": 7709, + "temperature": 2377, + "tTrend": 0, + "hTrend": 1, + "unit": 0, + "speak": 5, + "trend": 0, + "atType": 2, + "temperatureF": 7479, + "isOpenAutomation": 0, + "devTimeZone": "GMT-05:00", + "devSetting": { + "setId": None, + "devId": "1424979258063355749", + "externalPort": 4, + "devLight": 163, + "hasBacklightSwitch": 1, + "backlightSwitch": 1, + "devCt": 0, + "devCth": 0, + "devCh": 0, + "devTth": 0, + "devTt": 0, + "devTh": 0, + "devCompany": 0, + "vpdCth": 0, + "vpdCt": 0, + "vpdTransition": 0, + "devBth": 0, + "devBt": 0, + "devBh": 0, + "devBvpd": 0, + "isFlag": 0, + "onTimeSwitch": 0, + "onTime": 0, + "sensors": None, + "isOpenDoseTime": None, + "onDoseTime": None, + "offDoseTime": None, + "isOnMinMaxTime": None, + "onMinTime": None, + "onMaxTime": None, + "ecOrTds": None, + "ecUnit": None, + "tdsUnit": None, + "dualZoneSwitch": 1, + "photoCellSwitch": 1, + }, + "loadType": 0, + "loadState": 1, + "abnormalState": 0, + "devMacAddr": None, + "restore": False, + "masterPort": None, + "onlyUpdateSpeed": 0, +} + +DEVICE_SETTINGS_PAYLOAD = {"msg": "操作成功", "code": 200, "data": DEVICE_SETTING} + +DEVICE_SETTINGS = {str(DEVICE_ID): {1: DEVICE_SETTING}} + +DEVICE_INFO_LIST_ALL = [DEVICE_INFO] + +DEVICE_INFO_DATA = {str(DEVICE_ID): DEVICE_INFO} DEVICE_INFO_LIST_ALL_PAYLOAD = { "msg": "操作成功", "code": 200, "data": DEVICE_INFO_LIST_ALL, } + +ADD_DEV_MODE_PAYLOAD = {"msg": "操作成功", "code": 200} diff --git a/tests/test_ac_infinity.py b/tests/test_ac_infinity.py index 45b809a..fdad9d4 100644 --- a/tests/test_ac_infinity.py +++ b/tests/test_ac_infinity.py @@ -1,3 +1,6 @@ +import asyncio +from asyncio import Future + import pytest from homeassistant.helpers.update_coordinator import UpdateFailed from pytest_mock import MockFixture @@ -6,20 +9,26 @@ from custom_components.ac_infinity.ac_infinity import ACInfinity from custom_components.ac_infinity.client import ACInfinityClient from custom_components.ac_infinity.const import ( - DEVICE_KEY_DEVICE_NAME, - DEVICE_KEY_HUMIDITY, - DEVICE_KEY_MAC_ADDR, - DEVICE_KEY_TEMPERATURE, - DEVICE_PORT_KEY_NAME, - DEVICE_PORT_KEY_SPEAK, DOMAIN, MANUFACTURER, + PROPERTY_KEY_DEVICE_NAME, + PROPERTY_KEY_MAC_ADDR, + PROPERTY_PORT_KEY_NAME, + SENSOR_KEY_HUMIDITY, + SENSOR_KEY_TEMPERATURE, + SENSOR_PORT_KEY_SPEAK, + SETTING_KEY_AT_TYPE, + SETTING_KEY_OFF_SPEED, + SETTING_KEY_ON_SPEED, ) from .data_models import ( DEVICE_ID, + DEVICE_INFO_DATA, DEVICE_INFO_LIST_ALL, DEVICE_NAME, + DEVICE_SETTINGS, + DEVICE_SETTINGS_PAYLOAD, EMAIL, MAC_ADDR, PASSWORD, @@ -37,6 +46,11 @@ async def test_update_logged_in_should_be_called_if_not_logged_in( mocker.patch.object( ACInfinityClient, "get_all_device_info", return_value=DEVICE_INFO_LIST_ALL ) + mocker.patch.object( + ACInfinityClient, + "get_device_port_settings", + return_value=DEVICE_SETTINGS_PAYLOAD, + ) mockLogin: MockType = mocker.patch.object(ACInfinityClient, "login") ac_infinity = ACInfinity(EMAIL, PASSWORD) @@ -53,6 +67,11 @@ async def test_update_logged_in_should_not_be_called_if_not_necessary( mocker.patch.object( ACInfinityClient, "get_all_device_info", return_value=DEVICE_INFO_LIST_ALL ) + mocker.patch.object( + ACInfinityClient, + "get_device_port_settings", + return_value=DEVICE_SETTINGS_PAYLOAD, + ) mockLogin: MockType = mocker.patch.object(ACInfinityClient, "login") ac_infinity = ACInfinity(EMAIL, PASSWORD) @@ -66,88 +85,98 @@ async def test_update_data_set(self, mocker: MockFixture): mocker.patch.object( ACInfinityClient, "get_all_device_info", return_value=DEVICE_INFO_LIST_ALL ) + mocker.patch.object( + ACInfinityClient, + "get_device_port_settings", + return_value=DEVICE_SETTINGS_PAYLOAD, + ) mocker.patch.object(ACInfinityClient, "login") ac_infinity = ACInfinity(EMAIL, PASSWORD) await ac_infinity.update() - assert len(ac_infinity._data) == 1 - assert ac_infinity._data[0][DEVICE_KEY_DEVICE_NAME] == "Grow Tent" + assert len(ac_infinity._devices) == 1 + assert ( + ac_infinity._devices[str(DEVICE_ID)][PROPERTY_KEY_DEVICE_NAME] + == "Grow Tent" + ) @pytest.mark.parametrize( - "property, value", + "property_key, value", [ - (DEVICE_KEY_DEVICE_NAME, "Grow Tent"), - (DEVICE_KEY_MAC_ADDR, MAC_ADDR), - (DEVICE_KEY_TEMPERATURE, 2417), - (DEVICE_KEY_HUMIDITY, 7200), + (PROPERTY_KEY_DEVICE_NAME, "Grow Tent"), + (PROPERTY_KEY_MAC_ADDR, MAC_ADDR), + (SENSOR_KEY_TEMPERATURE, 2417), + (SENSOR_KEY_HUMIDITY, 7200), ], ) @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID)]) async def test_get_device_property_gets_correct_property( - self, device_id, property, value + self, device_id, property_key, value ): """getting a device property returns the correct value""" ac_infinity = ACInfinity(EMAIL, PASSWORD) - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA - result = ac_infinity.get_device_property(device_id, property) + result = ac_infinity.get_device_property(device_id, property_key) assert result == value @pytest.mark.parametrize( - "property, device_id", + "property_key, device_id", [ - (DEVICE_KEY_DEVICE_NAME, "232161"), + (PROPERTY_KEY_DEVICE_NAME, "232161"), ("MyFakeField", DEVICE_ID), ("MyFakeField", str(DEVICE_ID)), ], ) - async def test_get_device_property_returns_null_properly(self, property, device_id): + async def test_get_device_property_returns_null_properly( + self, property_key, device_id + ): """the absence of a value should return None instead of keyerror""" ac_infinity = ACInfinity(EMAIL, PASSWORD) - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA - result = ac_infinity.get_device_property(device_id, property) + result = ac_infinity.get_device_property(device_id, property_key) assert result is None @pytest.mark.parametrize( - "property, port_num, value", + "property_key, port_num, value", [ - (DEVICE_PORT_KEY_SPEAK, 1, 5), - (DEVICE_PORT_KEY_SPEAK, 2, 7), - (DEVICE_PORT_KEY_NAME, 3, "Circulating Fan"), - (DEVICE_PORT_KEY_NAME, 1, "Grow Lights"), + (SENSOR_PORT_KEY_SPEAK, 1, 5), + (SENSOR_PORT_KEY_SPEAK, 2, 7), + (PROPERTY_PORT_KEY_NAME, 3, "Circulating Fan"), + (PROPERTY_PORT_KEY_NAME, 1, "Grow Lights"), ], ) @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID)]) async def test_get_device_port_property_gets_correct_property( - self, device_id, port_num, property, value + self, device_id, port_num, property_key, value ): """getting a porp property gets the correct property from the correct port""" ac_infinity = ACInfinity(EMAIL, PASSWORD) - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA - result = ac_infinity.get_device_port_property(device_id, port_num, property) + result = ac_infinity.get_device_port_property(device_id, port_num, property_key) assert result == value @pytest.mark.parametrize( - "property, device_id, port_num", + "property_key, device_id, port_num", [ - (DEVICE_PORT_KEY_SPEAK, "232161", 1), + (SENSOR_PORT_KEY_SPEAK, "232161", 1), ("MyFakeField", DEVICE_ID, 1), - (DEVICE_PORT_KEY_SPEAK, DEVICE_ID, 9), + (SENSOR_PORT_KEY_SPEAK, DEVICE_ID, 9), ("MyFakeField", str(DEVICE_ID), 1), - (DEVICE_PORT_KEY_SPEAK, str(DEVICE_ID), 9), + (SENSOR_PORT_KEY_SPEAK, str(DEVICE_ID), 9), ], ) async def test_get_device_port_property_returns_null_properly( - self, property, device_id, port_num + self, property_key, device_id, port_num ): """the absence of a value should return None instead of keyerror""" ac_infinity = ACInfinity(EMAIL, PASSWORD) - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA - result = ac_infinity.get_device_port_property(device_id, port_num, property) + result = ac_infinity.get_device_port_property(device_id, port_num, property_key) assert result is None async def test_update_update_failed_thrown(self, mocker: MockFixture): @@ -167,7 +196,7 @@ async def test_update_update_failed_thrown(self, mocker: MockFixture): async def test_get_device_all_device_meta_data_returns_meta_data(self): """getting port device ids should return ids""" ac_infinity = ACInfinity(EMAIL, PASSWORD) - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA result = ac_infinity.get_all_device_meta_data() assert len(result) > 0 @@ -178,26 +207,26 @@ async def test_get_device_all_device_meta_data_returns_meta_data(self): assert device.mac_addr == MAC_ADDR assert [port.port_id for port in device.ports] == [1, 2, 3, 4] - @pytest.mark.parametrize("data", [[], None]) + @pytest.mark.parametrize("data", [{}, None]) async def test_get_device_all_device_meta_data_returns_empty_list(self, data): """getting device metadata returns empty list if no device exists or data isn't initialized""" ac_infinity = ACInfinity(EMAIL, PASSWORD) - ac_infinity._data = data + ac_infinity._devices = data result = ac_infinity.get_all_device_meta_data() assert result == [] @pytest.mark.parametrize( "devType,expected_model", - [(11, "Controller 69 Pro (CTR69P)"), (3, "Controller Type 3")], + [(11, "UIS Controller 69 Pro (CTR69P)"), (3, "UIS Controller Type 3")], ) async def test_ac_infinity_device_has_correct_device_info( self, devType: int, expected_model: str ): """getting device returns an model object that contains correct device info for the device registry""" ac_infinity = ACInfinity(EMAIL, PASSWORD) - ac_infinity._data = DEVICE_INFO_LIST_ALL - ac_infinity._data[0]["devType"] = devType + ac_infinity._devices = DEVICE_INFO_DATA + ac_infinity._devices[str(DEVICE_ID)]["devType"] = devType result = ac_infinity.get_all_device_meta_data() assert len(result) > 0 @@ -210,3 +239,63 @@ async def test_ac_infinity_device_has_correct_device_info( assert device_info.get("name") == DEVICE_NAME assert device_info.get("manufacturer") == MANUFACTURER assert device_info.get("model") == expected_model + + @pytest.mark.parametrize( + "setting_key, value", + [ + (SETTING_KEY_ON_SPEED, 5), + (SETTING_KEY_OFF_SPEED, 0), + (SETTING_KEY_AT_TYPE, 2), + ], + ) + @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID)]) + async def test_get_device_port_setting_gets_correct_property( + self, device_id, setting_key, value + ): + """getting a port setting gets the correct setting from the correct port""" + ac_infinity = ACInfinity(EMAIL, PASSWORD) + ac_infinity._devices = DEVICE_INFO_DATA + ac_infinity._port_settings = DEVICE_SETTINGS + + result = ac_infinity.get_device_port_setting(device_id, 1, setting_key) + assert result == value + + @pytest.mark.parametrize( + "setting_key, device_id", + [ + (SETTING_KEY_ON_SPEED, "232161"), + ("MyFakeField", DEVICE_ID), + (PROPERTY_PORT_KEY_NAME, DEVICE_ID), + ("MyFakeField", str(DEVICE_ID)), + (PROPERTY_PORT_KEY_NAME, str(DEVICE_ID)), + ], + ) + async def test_get_device_port_setting_returns_null_properly( + self, + setting_key, + device_id, + ): + """the absence of a value should return None instead of keyerror""" + ac_infinity = ACInfinity(EMAIL, PASSWORD) + ac_infinity._devices = DEVICE_INFO_DATA + ac_infinity._port_settings = DEVICE_SETTINGS + + result = ac_infinity.get_device_port_setting(device_id, 1, setting_key) + assert result is None + + async def test_set_device_port_setting(self, mocker: MockFixture): + future: Future = asyncio.Future() + future.set_result(None) + + mocker.patch.object(ACInfinityClient, "is_logged_in", return_value=True) + mocked_set = mocker.patch.object( + ACInfinityClient, "set_device_port_setting", return_value=future + ) + + ac_infinity = ACInfinity(EMAIL, PASSWORD) + ac_infinity._devices = DEVICE_INFO_DATA + ac_infinity._port_settings = DEVICE_SETTINGS + + await ac_infinity.set_device_port_setting(DEVICE_ID, 1, SETTING_KEY_AT_TYPE, 2) + + mocked_set.assert_called_with(DEVICE_ID, 1, SETTING_KEY_AT_TYPE, 2) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index f9c884e..d0d3373 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -15,10 +15,10 @@ async_setup_entry, ) from custom_components.ac_infinity.const import ( - DEVICE_PORT_KEY_ONLINE, DOMAIN, + SENSOR_PORT_KEY_ONLINE, ) -from tests.data_models import DEVICE_INFO_LIST_ALL, MAC_ADDR +from tests.data_models import DEVICE_INFO_DATA, MAC_ADDR EMAIL = "myemail@unittest.com" PASSWORD = "hunter2" @@ -45,7 +45,7 @@ def setup(mocker: MockFixture): ac_infinity = ACInfinity(EMAIL, PASSWORD) def set_data(): - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA return future mocker.patch.object(ACInfinity, "update", side_effect=set_data) @@ -97,13 +97,13 @@ async def test_async_setup_entry_plug_created_for_each_port(self, setup, port): """Sensor for device port connected is created on setup""" sensor = await self.__execute_and_get_port_sensor( - setup, port, DEVICE_PORT_KEY_ONLINE + setup, port, SENSOR_PORT_KEY_ONLINE ) assert "Status" in sensor._attr_name assert ( sensor._attr_unique_id - == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{DEVICE_PORT_KEY_ONLINE}" + == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{SENSOR_PORT_KEY_ONLINE}" ) assert sensor._attr_device_class == BinarySensorDeviceClass.PLUG @@ -121,7 +121,7 @@ async def test_async_update_plug_value_Correct(self, setup, port, expected): sensor: ACInfinityPortBinarySensorEntity = ( await self.__execute_and_get_port_sensor( - setup, port, DEVICE_PORT_KEY_ONLINE + setup, port, SENSOR_PORT_KEY_ONLINE ) ) await sensor.async_update() diff --git a/tests/test_client.py b/tests/test_client.py index b2ed4a0..ba7a381 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,15 +2,24 @@ from aioresponses import aioresponses from custom_components.ac_infinity.client import ( + API_URL_ADD_DEV_MODE, + API_URL_GET_DEV_MODE_SETTING, API_URL_GET_DEVICE_INFO_LIST_ALL, API_URL_LOGIN, ACInfinityClient, ACInfinityClientCannotConnect, ACInfinityClientInvalidAuth, + ACInfinityClientRequestFailed, +) +from custom_components.ac_infinity.const import ( + SETTING_KEY_ON_SPEED, + SETTING_KEY_SURPLUS, ) from tests.data_models import ( + ADD_DEV_MODE_PAYLOAD, DEVICE_ID, DEVICE_INFO_LIST_ALL_PAYLOAD, + DEVICE_SETTINGS_PAYLOAD, EMAIL, HOST, LOGIN_PAYLOAD, @@ -68,7 +77,7 @@ async def test_login_api_connect_error_raised_on_http_error(self, status_code): with pytest.raises(ACInfinityClientCannotConnect): await client.login() - @pytest.mark.parametrize("code", [500]) + @pytest.mark.parametrize("code", [400, 500]) async def test_login_api_auth_error_on_failed_login(self, code): """When login is called and returns a non-succesful status code, connect error should be raised""" @@ -83,6 +92,23 @@ async def test_login_api_auth_error_on_failed_login(self, code): with pytest.raises(ACInfinityClientInvalidAuth): await client.login() + @pytest.mark.parametrize("code", [400, 500]) + async def test_post_request_failed_error_on_failed_request(self, code): + """When login is called and returns a non-succesful status code, connect error should be raised""" + + with aioresponses() as mocked: + mocked.post( + f"{HOST}{API_URL_GET_DEVICE_INFO_LIST_ALL}", + status=200, + payload={"msg": "User Does Not Exist", "code": code}, + ) + + client = ACInfinityClient(HOST, EMAIL, PASSWORD) + client._user_id = USER_ID + + with pytest.raises(ACInfinityClientRequestFailed): + await client.get_all_device_info() + async def test_get_all_device_info_returns_user_devices(self): """When logged in, user devices should return a list of user devices""" client = ACInfinityClient(HOST, EMAIL, PASSWORD) @@ -105,3 +131,127 @@ async def test_get_all_device_info_connect_error_on_not_logged_in(self): client = ACInfinityClient(HOST, EMAIL, PASSWORD) with pytest.raises(ACInfinityClientCannotConnect): await client.get_all_device_info() + + async def test_get_device_port_settings_connect_error_on_not_logged_in(self): + """When not logged in, get user devices should throw a connect error""" + client = ACInfinityClient(HOST, EMAIL, PASSWORD) + with pytest.raises(ACInfinityClientCannotConnect): + await client.get_device_port_settings(DEVICE_ID, 1) + + async def test_set_device_port_setting_values_copied_from_get_call(self): + """When setting a value, first fetch the existing settings to build the payload""" + + client = ACInfinityClient(HOST, EMAIL, PASSWORD) + client._user_id = USER_ID + with aioresponses() as mocked: + mocked.post( + f"{HOST}{API_URL_GET_DEV_MODE_SETTING}", + status=200, + payload=DEVICE_SETTINGS_PAYLOAD, + ) + + mocked.post( + f"{HOST}{API_URL_ADD_DEV_MODE}", + status=200, + payload=ADD_DEV_MODE_PAYLOAD, + ) + + await client.set_device_port_setting(DEVICE_ID, 4, SETTING_KEY_ON_SPEED, 2) + + gen = (request for request in mocked.requests.values()) + _ = next(gen) + found = next(gen) + payload = found[0].kwargs["data"] + + assert payload["acitveTimerOff"] == 0 + assert payload["acitveTimerOn"] == 0 + assert payload["activeCycleOff"] == 0 + assert payload["activeCycleOn"] == 0 + assert payload["activeHh"] == 0 + assert payload["activeHt"] == 1 + assert payload["activeHtVpd"] == 0 + assert payload["activeHtVpdNums"] == 99 + assert payload["activeLh"] == 0 + assert payload["activeLt"] == 0 + assert payload["activeLtVpd"] == 0 + assert payload["activeLtVpdNums"] == 1 + assert payload["atType"] == 2 + assert payload["devHh"] == 100 + assert payload["devHt"] == 89 + assert payload["devHtf"] == 193 + assert payload["devId"] == "1424979258063355749" + assert payload["devLh"] == 0 + assert payload["devLt"] == 0 + assert payload["devLtf"] == 32 + assert payload["externalPort"] == 4 + assert payload["hTrend"] == 1 + assert payload["isOpenAutomation"] == 0 + assert payload["offSpead"] == 0 + assert payload["onlyUpdateSpeed"] == 0 + assert payload["schedEndtTime"] == 65535 + assert payload["schedStartTime"] == 65535 + assert payload["settingMode"] == 0 + assert payload["tTrend"] == 0 + assert payload["targetHumi"] == 0 + assert payload["targetHumiSwitch"] == 0 + assert payload["targetTSwitch"] == 0 + assert payload["targetTemp"] == 0 + assert payload["targetTempF"] == 32 + assert payload["targetVpd"] == 0 + assert payload["targetVpdSwitch"] == 0 + assert payload["trend"] == 0 + assert payload["unit"] == 0 + assert payload["vpdSettingMode"] == 0 + + async def test_set_device_port_setting_value_changed_in_payload(self): + """When setting a value, the value is updated in the built payload before sending""" + client = ACInfinityClient(HOST, EMAIL, PASSWORD) + client._user_id = USER_ID + with aioresponses() as mocked: + mocked.post( + f"{HOST}{API_URL_GET_DEV_MODE_SETTING}", + status=200, + payload=DEVICE_SETTINGS_PAYLOAD, + ) + + mocked.post( + f"{HOST}{API_URL_ADD_DEV_MODE}", + status=200, + payload=ADD_DEV_MODE_PAYLOAD, + ) + + await client.set_device_port_setting(DEVICE_ID, 4, SETTING_KEY_ON_SPEED, 2) + + gen = (request for request in mocked.requests.values()) + _ = next(gen) + found = next(gen) + payload = found[0].kwargs["data"] + + assert payload["onSpead"] == 2 + + async def test_set_device_port_setting_surplus_zero_even_when_null(self): + """When fetching existing settings before update, surplus should be set to 0 if existing is null""" + client = ACInfinityClient(HOST, EMAIL, PASSWORD) + client._user_id = USER_ID + with aioresponses() as mocked: + mocked.post( + f"{HOST}{API_URL_GET_DEV_MODE_SETTING}", + status=200, + payload=DEVICE_SETTINGS_PAYLOAD, + ) + + request_payload = ADD_DEV_MODE_PAYLOAD + request_payload[SETTING_KEY_SURPLUS] = None + + mocked.post( + f"{HOST}{API_URL_ADD_DEV_MODE}", status=200, payload=request_payload + ) + + await client.set_device_port_setting(DEVICE_ID, 4, SETTING_KEY_ON_SPEED, 2) + + gen = (request for request in mocked.requests.values()) + _ = next(gen) + found = next(gen) + payload = found[0].kwargs["data"] + + assert payload[SETTING_KEY_SURPLUS] == 0 diff --git a/tests/test_number.py b/tests/test_number.py index e31f78e..300d265 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -9,14 +9,15 @@ from custom_components.ac_infinity.ac_infinity import ACInfinity from custom_components.ac_infinity.const import ( - DEVICE_PORT_KEY_SPEAK, DOMAIN, + SETTING_KEY_OFF_SPEED, + SETTING_KEY_ON_SPEED, ) from custom_components.ac_infinity.number import ( ACInfinityPortNumberEntity, async_setup_entry, ) -from tests.data_models import DEVICE_INFO_LIST_ALL, MAC_ADDR +from tests.data_models import DEVICE_INFO_DATA, DEVICE_SETTINGS, MAC_ADDR EMAIL = "myemail@unittest.com" PASSWORD = "hunter2" @@ -43,10 +44,12 @@ def setup(mocker: MockFixture): ac_infinity = ACInfinity(EMAIL, PASSWORD) def set_data(): - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA + ac_infinity._port_settings = DEVICE_SETTINGS return future mocker.patch.object(ACInfinity, "update", side_effect=set_data) + mocker.patch.object(ACInfinity, "set_device_port_setting", return_value=future) mocker.patch.object(ConfigEntry, "__init__", return_value=None) mocker.patch.object(HomeAssistant, "__init__", return_value=None) @@ -88,28 +91,48 @@ async def test_async_setup_all_sensors_created(self, setup): await async_setup_entry(hass, configEntry, entities.add_entities_callback) - assert len(entities._added_entities) == 4 + assert len(entities._added_entities) == 8 - async def test_async_setup_entry_intensity_created_for_each_port(self, setup): + @pytest.mark.parametrize( + "setting", [(SETTING_KEY_OFF_SPEED), (SETTING_KEY_ON_SPEED)] + ) + async def test_async_setup_entry_current_speed_created_for_each_port( + self, setup, setting + ): """Sensor for device port intensity created on setup""" - sensor = await self.__execute_and_get_port_sensor(setup, DEVICE_PORT_KEY_SPEAK) + sensor = await self.__execute_and_get_port_sensor(setup, setting) - assert "Intensity" in sensor._attr_name - assert ( - sensor._attr_unique_id - == f"{DOMAIN}_{MAC_ADDR}_port_1_{DEVICE_PORT_KEY_SPEAK}" - ) + assert "Speed" in sensor._attr_name + assert sensor._attr_unique_id == f"{DOMAIN}_{MAC_ADDR}_port_1_{setting}" assert sensor._attr_device_class == NumberDeviceClass.POWER_FACTOR assert sensor._attr_native_min_value == 0 assert sensor._attr_native_max_value == 10 - async def test_async_update_intensity_value_Correct(self, setup): + @pytest.mark.parametrize( + "setting,expected", [(SETTING_KEY_OFF_SPEED, 0), (SETTING_KEY_ON_SPEED, 5)] + ) + async def test_async_update_current_speed_value_Correct( + self, setup, setting, expected + ): """Reported sensor value matches the value in the json payload""" sensor: ACInfinityPortNumberEntity = await self.__execute_and_get_port_sensor( - setup, DEVICE_PORT_KEY_SPEAK + setup, setting ) await sensor.async_update() - assert sensor._attr_native_value == 5 + assert sensor._attr_native_value == expected + + @pytest.mark.parametrize( + "setting,expected", [(SETTING_KEY_OFF_SPEED, 0), (SETTING_KEY_ON_SPEED, 5)] + ) + async def test_async_set_native_value(self, setup, setting, expected): + """Reported sensor value matches the value in the json payload""" + + sensor: ACInfinityPortNumberEntity = await self.__execute_and_get_port_sensor( + setup, setting + ) + await sensor.async_set_native_value(4) + + assert sensor._attr_native_value == 4 diff --git a/tests/test_select.py b/tests/test_select.py new file mode 100644 index 0000000..d1a80c2 --- /dev/null +++ b/tests/test_select.py @@ -0,0 +1,176 @@ +import asyncio +from asyncio import Future + +import pytest +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from pytest_mock import MockFixture + +from custom_components.ac_infinity.ac_infinity import ACInfinity +from custom_components.ac_infinity.const import ( + DOMAIN, + SETTING_KEY_AT_TYPE, +) +from custom_components.ac_infinity.select import ( + ACInfinityPortSelectEntity, + async_setup_entry, +) +from tests.data_models import DEVICE_ID, DEVICE_INFO_DATA, DEVICE_SETTINGS, MAC_ADDR + +EMAIL = "myemail@unittest.com" +PASSWORD = "hunter2" +ENTRY_ID = f"ac_infinity-{EMAIL}" + + +class EntitiesTracker: + def __init__(self) -> None: + self._added_entities: list[ACInfinityPortSelectEntity] = [] + + def add_entities_callback( + self, + new_entities: list[ACInfinityPortSelectEntity], + update_before_add: bool = False, + ): + self._added_entities = new_entities + + +@pytest.fixture +def setup(mocker: MockFixture): + future: Future = asyncio.Future() + future.set_result(None) + + ac_infinity = ACInfinity(EMAIL, PASSWORD) + + def set_data(): + ac_infinity._devices = DEVICE_INFO_DATA + ac_infinity._port_settings = DEVICE_SETTINGS + + return future + + mocker.patch.object(ACInfinity, "update", side_effect=set_data) + mocker.patch.object(ACInfinity, "set_device_port_setting", return_value=future) + mocker.patch.object(ConfigEntry, "__init__", return_value=None) + mocker.patch.object(HomeAssistant, "__init__", return_value=None) + + hass = HomeAssistant("/path") + hass.data = {DOMAIN: {ENTRY_ID: ac_infinity}} + + configEntry = ConfigEntry() + configEntry.entry_id = ENTRY_ID + + entities = EntitiesTracker() + + return (hass, configEntry, entities, ac_infinity) + + +@pytest.mark.asyncio +class TestNumbers: + set_data_mode_value = 0 + + async def __execute_and_get_port_sensor( + self, setup, property_key: str + ) -> ACInfinityPortSelectEntity: + entities: EntitiesTracker + (hass, configEntry, entities, _) = setup + + await async_setup_entry(hass, configEntry, entities.add_entities_callback) + + found = [ + sensor + for sensor in entities._added_entities + if property_key in sensor._attr_unique_id + and "port_1" in sensor._attr_unique_id + ] + assert len(found) == 1 + + return found[0] + + async def test_async_setup_all_sensors_created(self, setup): + """All sensors created""" + entities: EntitiesTracker + (hass, configEntry, entities, _) = setup + + await async_setup_entry(hass, configEntry, entities.add_entities_callback) + + assert len(entities._added_entities) == 4 + + async def test_async_setup_mode_created_for_each_port(self, setup): + """Sensor for device port mode created on setup""" + + sensor = await self.__execute_and_get_port_sensor(setup, SETTING_KEY_AT_TYPE) + + assert "Mode" in sensor._attr_name + assert ( + sensor._attr_unique_id + == f"{DOMAIN}_{MAC_ADDR}_port_1_{SETTING_KEY_AT_TYPE}" + ) + assert len(sensor._attr_options) == 8 + + @pytest.mark.parametrize( + "atType,expected", + [ + (1, "Off"), + (2, "On"), + (3, "Auto"), + (4, "Timer to On"), + (5, "Timer to Off"), + (6, "Cycle"), + (7, "Schedule"), + (8, "VPD"), + ], + ) + async def test_async_update_mode_value_Correct( + self, setup, mocker: MockFixture, atType, expected + ): + """Reported sensor value matches the value in the json payload""" + ac_infinity: ACInfinity + + (_, _, _, ac_infinity) = setup + sensor: ACInfinityPortSelectEntity = await self.__execute_and_get_port_sensor( + setup, SETTING_KEY_AT_TYPE + ) + + def set_data(): + future: Future = asyncio.Future() + future.set_result(None) + + ac_infinity._devices = DEVICE_INFO_DATA + ac_infinity._port_settings = DEVICE_SETTINGS + ac_infinity._port_settings[str(DEVICE_ID)][1][SETTING_KEY_AT_TYPE] = atType + return future + + mocker.patch.object(ACInfinity, "update", side_effect=set_data) + await sensor.async_update() + + assert sensor._attr_current_option == expected + + @pytest.mark.parametrize( + "expected,atTypeString", + [ + (1, "Off"), + (2, "On"), + (3, "Auto"), + (4, "Timer to On"), + (5, "Timer to Off"), + (6, "Cycle"), + (7, "Schedule"), + (8, "VPD"), + ], + ) + async def test_async_set_native_value( + self, mocker: MockFixture, setup, atTypeString, expected + ): + """Reported sensor value matches the value in the json payload""" + future: Future = asyncio.Future() + future.set_result(None) + + mock_set = mocker.patch.object( + ACInfinity, "set_device_port_setting", return_value=future + ) + sensor: ACInfinityPortSelectEntity = await self.__execute_and_get_port_sensor( + setup, SETTING_KEY_AT_TYPE + ) + await sensor.async_select_option(atTypeString) + + assert sensor._attr_current_option == atTypeString + mock_set.assert_called_with(str(DEVICE_ID), 1, SETTING_KEY_AT_TYPE, expected) diff --git a/tests/test_sensor.py b/tests/test_sensor.py index fcf2699..fde6194 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -14,18 +14,18 @@ from custom_components.ac_infinity.ac_infinity import ACInfinity from custom_components.ac_infinity.const import ( - DEVICE_KEY_HUMIDITY, - DEVICE_KEY_TEMPERATURE, - DEVICE_KEY_VAPOR_PRESSURE_DEFICIT, - DEVICE_PORT_KEY_SPEAK, DOMAIN, + SENSOR_KEY_HUMIDITY, + SENSOR_KEY_TEMPERATURE, + SENSOR_KEY_VPD, + SENSOR_PORT_KEY_SPEAK, ) from custom_components.ac_infinity.sensor import ( ACInfinityPortSensorEntity, ACInfinitySensorEntity, async_setup_entry, ) -from tests.data_models import DEVICE_INFO_LIST_ALL, MAC_ADDR +from tests.data_models import DEVICE_INFO_DATA, MAC_ADDR EMAIL = "myemail@unittest.com" PASSWORD = "hunter2" @@ -52,7 +52,7 @@ def setup(mocker: MockFixture): ac_infinity = ACInfinity(EMAIL, PASSWORD) def set_data(): - ac_infinity._data = DEVICE_INFO_LIST_ALL + ac_infinity._devices = DEVICE_INFO_DATA return future mocker.patch.object(ACInfinity, "update", side_effect=set_data) @@ -119,10 +119,10 @@ async def test_async_setup_all_sensors_created(self, setup): async def test_async_setup_entry_temperature_created(self, setup): """Sensor for device reported temperature is created on setup""" - sensor = await self.__execute_and_get_sensor(setup, DEVICE_KEY_TEMPERATURE) + sensor = await self.__execute_and_get_sensor(setup, SENSOR_KEY_TEMPERATURE) assert "Temperature" in sensor._attr_name - assert sensor._attr_unique_id == f"{DOMAIN}_{MAC_ADDR}_{DEVICE_KEY_TEMPERATURE}" + assert sensor._attr_unique_id == f"{DOMAIN}_{MAC_ADDR}_{SENSOR_KEY_TEMPERATURE}" assert sensor._attr_device_class == SensorDeviceClass.TEMPERATURE assert sensor._attr_native_unit_of_measurement == UnitOfTemperature.CELSIUS @@ -130,7 +130,7 @@ async def test_async_update_temperature_value_Correct(self, setup): """Reported sensor value matches the value in the json payload""" sensor: ACInfinitySensorEntity = await self.__execute_and_get_sensor( - setup, DEVICE_KEY_TEMPERATURE + setup, SENSOR_KEY_TEMPERATURE ) await sensor.async_update() @@ -139,10 +139,10 @@ async def test_async_update_temperature_value_Correct(self, setup): async def test_async_setup_entry_humidity_created(self, mocker, setup): """Sensor for device reported humidity is created on setup""" - sensor = await self.__execute_and_get_sensor(setup, DEVICE_KEY_HUMIDITY) + sensor = await self.__execute_and_get_sensor(setup, SENSOR_KEY_HUMIDITY) assert "Humidity" in sensor._attr_name - assert sensor._attr_unique_id == f"{DOMAIN}_{MAC_ADDR}_{DEVICE_KEY_HUMIDITY}" + assert sensor._attr_unique_id == f"{DOMAIN}_{MAC_ADDR}_{SENSOR_KEY_HUMIDITY}" assert sensor._attr_device_class == SensorDeviceClass.HUMIDITY assert sensor._attr_native_unit_of_measurement == PERCENTAGE @@ -150,7 +150,7 @@ async def test_async_update_humidity_value_Correct(self, setup): """Reported sensor value matches the value in the json payload""" sensor: ACInfinitySensorEntity = await self.__execute_and_get_sensor( - setup, DEVICE_KEY_HUMIDITY + setup, SENSOR_KEY_HUMIDITY ) await sensor.async_update() @@ -159,15 +159,10 @@ async def test_async_update_humidity_value_Correct(self, setup): async def test_async_setup_entry_vpd_created(self, mocker, setup): """Sensor for device reported humidity is created on setup""" - sensor = await self.__execute_and_get_sensor( - setup, DEVICE_KEY_VAPOR_PRESSURE_DEFICIT - ) + sensor = await self.__execute_and_get_sensor(setup, SENSOR_KEY_VPD) assert "VPD" in sensor._attr_name - assert ( - sensor._attr_unique_id - == f"{DOMAIN}_{MAC_ADDR}_{DEVICE_KEY_VAPOR_PRESSURE_DEFICIT}" - ) + assert sensor._attr_unique_id == f"{DOMAIN}_{MAC_ADDR}_{SENSOR_KEY_VPD}" assert sensor._attr_device_class == SensorDeviceClass.PRESSURE assert sensor._attr_native_unit_of_measurement == UnitOfPressure.KPA @@ -175,7 +170,7 @@ async def test_async_update_vpd_value_Correct(self, setup): """Reported sensor value matches the value in the json payload""" sensor: ACInfinitySensorEntity = await self.__execute_and_get_sensor( - setup, DEVICE_KEY_VAPOR_PRESSURE_DEFICIT + setup, SENSOR_KEY_VPD ) await sensor.async_update() @@ -188,13 +183,13 @@ async def test_async_setup_entry_current_power_created_for_each_port( """Sensor for device port speak created on setup""" sensor = await self.__execute_and_get_port_sensor( - setup, port, DEVICE_PORT_KEY_SPEAK + setup, port, SENSOR_PORT_KEY_SPEAK ) - assert "Power" in sensor._attr_name + assert "Current Speed" in sensor._attr_name assert ( sensor._attr_unique_id - == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{DEVICE_PORT_KEY_SPEAK}" + == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{SENSOR_PORT_KEY_SPEAK}" ) assert sensor._attr_device_class == SensorDeviceClass.POWER_FACTOR @@ -213,7 +208,7 @@ async def test_async_update_current_power_value_Correct( """Reported sensor value matches the value in the json payload""" sensor: ACInfinityPortSensorEntity = await self.__execute_and_get_port_sensor( - setup, port, DEVICE_PORT_KEY_SPEAK + setup, port, SENSOR_PORT_KEY_SPEAK ) await sensor.async_update()