From 2a83391e480aff357486eb5ec2d9c387f5f847b3 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Thu, 8 Aug 2024 16:50:58 -0500 Subject: [PATCH] Feature/dynamic response settings (#62) * reorganize readme * add device settings for dynamic reponses * tests * pre-commit * implement suitable_fn pattern * account for port settings being a child item in the /getdevModeSettingList payload * coverage and bug fixes * rename some things * seperate concept of control vs setting have settings properly updating advanced settings instead of device mode settings. * pre-commit * bug fixes and finishing touches * fix CI codestyle check * remove empty comment --- .github/workflows/style.yaml | 2 +- README.md | 211 ++++++---- .../ac_infinity/binary_sensor.py | 20 +- custom_components/ac_infinity/client.py | 75 ++-- custom_components/ac_infinity/const.py | 23 +- custom_components/ac_infinity/core.py | 349 ++++++++++++++-- custom_components/ac_infinity/manifest.json | 2 +- custom_components/ac_infinity/number.py | 371 +++++++++++++---- custom_components/ac_infinity/select.py | 68 +++- custom_components/ac_infinity/sensor.py | 45 ++- custom_components/ac_infinity/strings.json | 21 + custom_components/ac_infinity/switch.py | 75 ++-- custom_components/ac_infinity/time.py | 23 +- .../ac_infinity/translations/en.json | 21 + tests/__init__.py | 48 ++- tests/data_models.py | 151 +++---- tests/test_client.py | 233 +++++------ tests/test_core.py | 319 ++++++++++++--- tests/test_number.py | 376 +++++++++++++++--- tests/test_select.py | 101 ++++- tests/test_sensor.py | 14 +- tests/test_switch.py | 90 ++--- tests/test_time.py | 18 +- 23 files changed, 1942 insertions(+), 714 deletions(-) diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml index 4f90a7c..acdc9dc 100644 --- a/.github/workflows/style.yaml +++ b/.github/workflows/style.yaml @@ -18,7 +18,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt - name: Ruff - run: ruff . + run: ruff check . - name: Black run : black . - name: Codespell diff --git a/README.md b/README.md index 10c7d6a..7677384 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,34 @@ This is a custom component for [Home Assistant](http://home-assistant.io) that adds support for [AC Infinity](https://acinfinity.com/) grow tent devices within the [Smart UIS Controller](https://acinfinity.com/smart-controllers/) cloud ecosystem. -## Compatibility +- [Compatibility](#compatibility) +- [Installation](#installation) + - [HACS](#hacs) + - [Manual Installation](#manual-installation) +- [Initial Setup](#initial-setup) + - [Additional Configuration](#additional-configuration) +- [Entities](#entities) + - [Terms](#terms) + - [Controller Sensors](#controller-sensors) + - [Device Sensors](#device-sensors) + - [Device Controls](#device-controls) + - [Global](#global) + - [On Mode](#on-mode) + - [Off Mode](#off-mode) + - [Auto Mode](#auto-mode) + - [Timer to On Mode](#timer-to-on-mode) + - [Timer to Off Mode](#timer-to-off-mode) + - [Cycle Mode](#cycle-mode) + - [Schedule Mode](#schedule-mode) + - [VPD Mode](#vpd-mode) + - [Controller Settings](#controller-settings) + - [Sensor / VPD Calibration](#sensor--vpd-calibration) + - [Device Settings](#device-settings) + - [Dynamic Response](#dynamic-response) + - [Transition Mode](#transition-mode) + - [Buffer Mode](#buffer-mode) + +# Compatibility This integration is compatible with the following UIS Controllers @@ -21,9 +48,9 @@ This integration is compatible with the following UIS Controllers This integration requires the controller be connected to Wifi, and thus is not compatible with bluetooth only devices such as Controller 67 or the base model of Controller 69, as they do not sync directly to the UIS Cloud -## Installation +# Installation -### HACS +## HACS This integration is made available through the Home Assistant Community Store default feed. Simply search for "AC Infinity" and install it directly from HACS. @@ -31,15 +58,15 @@ This integration is made available through the Home Assistant Community Store de Please see the [official HACS documentation](https://hacs.xyz) for information on how to install and use HACS. -### Manual Installation +## Manual Installation Copy `custom_components/acinfinity` into your Home Assistant `$HA_HOME/config` directory, then restart Home Assistant -## Initial Setup +# Initial Setup Add an integration entry as normal from integration section of the home assistant settings. You'll need the following configuration items -- **Email**: The e-mail registered with your AC Infinity account. -- **Password**: The password for this account. +- `Email`: The e-mail registered with your AC Infinity account. +- `Password`: The password for this account. ![Initial-Setup](/images/initial-setup.png) @@ -47,94 +74,134 @@ Add an integration entry as normal from integration section of the home assistan After adding an integration entry, the following additional configurations can be modified via the configuration options dialog. -- **Polling Interval (Seconds)**: The time between update calls to the AC Infinity API. Minimum allowed polling interval is 5 seconds. -- **Update Password**: When provided, updates the password used to connect to your AC Infinity account. Requires Home Assistant restart. +- `Polling Interval (Seconds)`: The time between update calls to the AC Infinity API. Minimum allowed polling interval is 5 seconds. +- `Update Password`: When provided, updates the password used to connect to your AC Infinity account. Requires Home Assistant restart. ![Additional-Configuration](/images/additional-configuration.png) -## Data Available +# Entities -This integration will create a device for each AC Infinity Controller on the configured user account. Each device will have the following sensors created. +Controller entities will be created for each AC Infinity Controller on the configured user account. -- Humidity -- Air Temperature -- Vaper Pressure Deficit (VPD) +Device entities will be created for each ***PORT*** on each UIS controller, even if no device is attached to a given port. The UIS protocol is device type agnostic, so each port will be treated the same regardless of what is plugged (or not plugged) into it. -Sensors will also be created for each ***PORT*** on a controller, even if no device is attached. The UIS protocol is device type agnostic, so each port will be treated the same regardless of what is plugged (or not plugged) into it. +## Terms +- `Sesonr`: A read-only measurement entity, such as temperature or humidity. +- `Control`: An entity that can change the operational state of devices, such as individual mode selections, triggers, and timer schedules. +- `Setting`: An entity that can change controller/device settings. These correspond to fields found in the Settings section of the Android/iOS app. -- Status - Is there a device plugged in on that port -- Power - Current Power supplied to the connected device +## Controller Sensors +Read Only sensors reported from the controller +- `Air Temperature`: The air temperature as reported by the air probe. +- `Humidity`: The humidity as reported by the air probe. +- `Vaper Pressure Deficit (VPD)`: Calculated VPD based on air probe temperature and humidity readings. + +## Device Sensors +Read Only sensors reported from each device +- `Status`: Di there a device plugged in on that port +- `Power`: Current Power supplied to the connected device ![AC-Infinity](/images/ac-infinity-device.png) +## Device Controls +Read/Write controls that define if a device runs in an ON or OFF state. Each control is associated to a mode, and is only relevant when the device is operating in that mode. + +The mode can be changed via the `Active Mode` control, which provides the following options. +- `On`: Device is always set to the on speed +- `Off`: Device is always set to the off speed +- `Auto`: Device toggled based on temperature and/or humidity triggers +- `Timer to On`: Device is turned on after a set duration +- `Timer to Off`: Device is turned off after a set duration +- `Cycle`: Device is toggled after set intervals +- `Schedule`: Device is toggled based on a schedule +- `VPD`: Device is toggled based on VPD triggers + +### Global +These settings control the power level of a device when in a given trigger state, which is shared across all modes. +- `On Power`: Go to OFF MODE to set. The device will run at this level when triggered ON +- `Off Power`: Go to ON MODE to set. The device will run at this level even when triggered OFF + +### On Mode +Device is always set to the on speed . This mode has no unique controls. + +### Off Mode +Device is always set to the off speed . This mode has no unique controls. + +### Auto Mode +Device toggled based on temperature and/or humidity triggers +- `High Temp Enabled`: Enable or disable high temp trigger while in Auto mode +- `High Temp Trigger`: If trigger is enabled, device will be turned on if temp exceeds configured value. +- `Low Temp Enabled`: Enable or disable low temp trigger while in Auto mode +- `Low Temp Trigger`: If trigger is enabled, device will be turned on if temp drops below configured value. +- `High Humidity Enabled`: Enable or disable high humidity trigger while in Auto mode +- `High Humidity Trigger`: If trigger is enabled, device will be turned on if humidity exceeds configured value. +- `Low Humidity Enabled`: Enable or disable low humidity trigger while in Auto mode +- `Low Humidity Trigger`: If trigger is enabled, device will be turned on if humidity drops below configured value. + +### Timer to On Mode +Device is turned on after a set duration +- `Minutes to On`: Device will be turned on after the configured number of minutes + +### Timer to Off Mode +Device is turned off after a set duration +- `Minutes to On`: Device will be turned off after the configured number of minutes + +### Cycle Mode +Device is toggled after set intervals +- `Cycle Minutes On`: The amount of minutes the device will stay in on mode before switching to off mode +- `Cycle Minutes Off`: The amount of minutes the device will stay in off mode before switching to on mode + +### Schedule Mode +Device is toggled based on a schedule +- `Schedule Start Time`: The time that the device will switch into on mode daily +- `Schedule End Time`: The time that the device will switch into off mode daily + +### VPD Mode +Device is toggled based on VPD triggers +- `VPD High Enabled`: Enable or disable high VPD trigger while in VPD mode +- `VPD High Trigger`: If trigger is enabled, device will be turned on if VPD exceeds configured value. +- `VPD Low Enabled`: Enable or disable low VPD trigger while in VPD mode +- `VPD Low Trigger`: If trigger is enabled, device will be turned on if VPD drops below configured value. + ## Controller Settings -The following controls will be created for each UIS Controller attached to the configured account. +### Sensor / VPD Calibration +These entities correspond to fields found in the `Controller` tab of the device settings in the AC Infinity App. -- **Calibrate Temperature**: Adjusts the temperature reading from the sensor probe, up to ±10C or ±20F -- **Calibrate Humidity**: Adjusts the humidity reading from the sensor probe, up to ±10% -- **VPD Leaf Temperature Offset**: Adjusts the leaf temperature in VPD calculation, up to ±10C or ±20F +- `Calibrate Temperature`: Adjusts the temperature reading from the sensor probe, up to ±10C or ±20F +- `Calibrate Humidity`: Adjusts the humidity reading from the sensor probe, up to ±10% +- `VPD Leaf Temperature Offset`: Adjusts the leaf temperature in VPD calculation, up to ±10C or ±20F + Note: If the preferred unit of temperature is changed on the UIS Controller, a reboot of Home Assistant is required to update the user interface controls with the correct min/max values. That being said, these fields should still continue to function correctly when interfacing with the UIS API, even without a reboot. + ## Device Settings +These entities correspond to fields found in the `Port` tab of the device settings in the AC Infinity App. -The following controls will be created for each ***PORT*** on each UIS controller, even if no device is attached to a given port. The UIS protocol is device type agnostic, so each port will be treated the same regardless of what is plugged (or not plugged) into it. +### Level Status +These settings control the power level of a device when in a given trigger state. +- `On Speed`: Go to OFF MODE to set. The device will run at this level when triggered ON +- `Off Speed`: Go to ON MODE to set. The device will run at this level even when triggered OFF -The mode can be changed via the mode control. The following documentation is split into controls relevant to each mode. -- **On**: Device is always set to the on speed -- **Off**: Device is always set to the off speed -- **Auto**: Device toggled based on temperature and/or humidity triggers -- **Timer to On**: Device is turned on after a set duration -- **Timer to Off**: Device is turned off after a set duration -- **VPD**: Device is toggled based on VPD triggers -- **Cycle**: Device is toggled after set intervals -- **Schedule**: Device is toggled based on a schedule +### Dynamic Response -### On Mode -- **On Speed**: The speed/intensity of the device while in on mode +The dynamic response type can be changed via the `Dynamic Response` setting. +- `Transition`: UIS Devices will ramp up in levels when trigger to run in AUTO and VPD Modes (see Device Controls section below). Set a transition threshold X. For every multiple of X that the probe temperature, humidity and VPD has surpassed your trigger points, the UIS Device will increase by one level. +- `Buffer`: UIS and Outlet Devices will have a gap created on their temperature, humidity, and VPD triggers to prevent devices from turning on and off too frequently. -### Off Mode -- **Off Speed**: The speed/intensity of the device while in off mode +#### Transition Mode +- `Transition Temperature`: Set a transition threshold X. For every multiple of X that the probe temperature has surpassed your trigger points, the UIS Device will increase by one level. +- `Transition Humidity`: Set a transition threshold X. For every multiple of X that the probe humidity has surpassed your trigger points, the UIS Device will increase by one level. +- `Transition VPD`: Set a transition threshold X. For every multiple of X that the probe VPD has surpassed your trigger points, the UIS Device will increase by one level. -### Auto Mode -- **On Speed**: The speed/intensity of the device while in on mode -- **Off Speed**: The speed/intensity of the device while in off mode -- **Auto High Temp Enabled**: Enable or disable high temp trigger while in Auto mode -- **Auto High Temp Trigger**: If trigger is enabled, device will be turned on if temp exceeds configured value. -- **Auto Low Temp Enabled**: Enable or disable low temp trigger while in Auto mode -- **Auto Low Temp Trigger**: If trigger is enabled, device will be turned on if temp drops below configured value. -- **Auto High Humidity Enabled**: Enable or disable high humidity trigger while in Auto mode -- **Auto High Humidity Trigger**: If trigger is enabled, device will be turned on if humidity exceeds configured value. -- **Auto Low Humidity Enabled**: Enable or disable low humidity trigger while in Auto mode -- **Auto Low Humidity Trigger**: If trigger is enabled, device will be turned on if humidity drops below configured value. - -### Timer to On -- **On Speed**: The speed/intensity of the device while in on mode -- **Off Speed**: The speed/intensity of the device while in off mode -- **Minutes to On**: Device will be turned on after the configured number of minutes - -### Timer to Off -- **On Speed**: The speed/intensity of the device while in on mode -- **Off Speed**: The speed/intensity of the device while in off mode -- **Minutes to On**: Device will be turned off after the configured number of minutes +    [Official Documentation](https://acinfinity.com/pages/controller-programming/transition-setting.html) +#### Buffer Mode -### Cycle Mode -- **On Speed**: The speed/intensity of the device while in on mode -- **Off Speed**: The speed/intensity of the device while in off mode -- **Cycle Minutes On**: The amount of minutes the device will stay in on mode before switching to off mode -- **Cycle Minutes Off**: The amounto f minutes the device will stay in off mode before switching to on mode +- `Buffer Temperature`: Set a buffer X. Triggers won't deactivate until the temperature falls X degrees below the trigger temperature for high triggers, or X degrees above the trigger temperature for low triggers. +- `Buffer Humidity`: Set a buffer X. Triggers won't deactivate until the humidity falls X percentage points below the trigger humidity for high triggers, or X percentage points above the trigger humidity for low triggers. +- `Buffer VPD`: Set a buffer X. Triggers won't deactivate until the VPD falls X kPa below the trigger VPD for high triggers, or X kPa above the trigger VPD for low triggers. -### Schedule Mode -- **On Speed**: The speed/intensity of the device while in on mode -- **Off Speed**: The speed/intensity of the device while in off mode -- **Schedule Start Time**: The time that the device will switch into on mode daily -- **Schedule End Time**: The time that the device will switch into off mode daily - -### VPD Mode -- **VPD High Enabled**: Enable or disable high VPD trigger while in VPD mode -- **VPD High Trigger**: If trigger is enabled, device will be turned on if VPD exceeds configured value. -- **VPD Low Enabled**: Enable or disable low VPD trigger while in VPD mode -- **VPD Low Trigger**: If trigger is enabled, device will be turned on if VPD drops below configured value. +    [Official Documentation](https://acinfinity.com/pages/controller-programming/buffer-setting.html) diff --git a/custom_components/ac_infinity/binary_sensor.py b/custom_components/ac_infinity/binary_sensor.py index 3a9c184..352a014 100644 --- a/custom_components/ac_infinity/binary_sensor.py +++ b/custom_components/ac_infinity/binary_sensor.py @@ -14,10 +14,12 @@ from .core import ( ACInfinityDataUpdateCoordinator, + ACInfinityEntities, ACInfinityPort, ACInfinityPortEntity, ACInfinityPortReadOnlyMixin, get_value_fn_port_property_default, + suitable_fn_port_property_default, ) _LOGGER = logging.getLogger(__name__) @@ -46,6 +48,7 @@ class ACInfinityPortBinarySensorEntityDescription( device_class=BinarySensorDeviceClass.PLUG, icon="mdi:power", translation_key="port_online", + suitable_fn=suitable_fn_port_property_default, get_value_fn=get_value_fn_port_property_default, ) ] @@ -68,7 +71,13 @@ def __init__( description: haas description used to initialize the entity. port: port object the entity is bound to """ - super().__init__(coordinator, port, description.key) + super().__init__( + coordinator, + port, + description.suitable_fn, + description.key, + Platform.BINARY_SENSOR, + ) self.entity_description = description @property @@ -85,18 +94,13 @@ async def async_setup_entry( coordinator: ACInfinityDataUpdateCoordinator = hass.data[DOMAIN][config.entry_id] controllers = coordinator.ac_infinity.get_all_controller_properties() - entities: list[ACInfinityPortBinarySensorEntity] = [] + entities = ACInfinityEntities() for controller in controllers: for port in controller.ports: for description in PORT_DESCRIPTIONS: entity = ACInfinityPortBinarySensorEntity( coordinator, description, port ) - entities.append(entity) - _LOGGER.info( - 'Initializing entity "%s" for platform "%s".', - entity.unique_id, - Platform.BINARY_SENSOR, - ) + entities.append_if_suitable(entity) add_entities_callback(entities) diff --git a/custom_components/ac_infinity/client.py b/custom_components/ac_infinity/client.py index 09517c2..f8255f0 100644 --- a/custom_components/ac_infinity/client.py +++ b/custom_components/ac_infinity/client.py @@ -5,7 +5,7 @@ import async_timeout from homeassistant.exceptions import HomeAssistantError -from custom_components.ac_infinity.const import ControllerSettingKey, PortSettingKey +from custom_components.ac_infinity.const import AdvancedSettingsKey, PortControlKey _LOGGER = logging.getLogger(__name__) @@ -97,24 +97,24 @@ async def set_device_mode_settings( # Remove fields that are not part of update payload, as well as the devSettings structure so we're not messing # with the controller settings. for key in [ - PortSettingKey.DEVICE_MAC_ADDR, - PortSettingKey.IPC_SETTING, - PortSettingKey.DEV_SETTING, + PortControlKey.DEVICE_MAC_ADDR, + PortControlKey.IPC_SETTING, + PortControlKey.DEV_SETTING, ]: if key in settings: del settings[key] # Add defaulted fields that exist in the update call on the phone app, but may not exist in the fetch call for key in [ - PortSettingKey.VPD_STATUS, - PortSettingKey.VPD_NUMS, + PortControlKey.VPD_STATUS, + PortControlKey.VPD_NUMS, ]: if key not in settings: settings[key] = 0 # Convert ids that are strings on the fetch call to int values for the update call - settings[PortSettingKey.DEV_ID] = int(settings[PortSettingKey.DEV_ID]) - settings[PortSettingKey.MODE_SET_ID] = int(settings[PortSettingKey.MODE_SET_ID]) + settings[PortControlKey.DEV_ID] = int(settings[PortControlKey.DEV_ID]) + settings[PortControlKey.MODE_SET_ID] = int(settings[PortControlKey.MODE_SET_ID]) # Set values changed by the user for key, value in key_values: @@ -128,82 +128,83 @@ async def set_device_mode_settings( headers = self.__create_headers(use_auth_token=True) _ = await self.__post(API_URL_ADD_DEV_MODE, settings, headers) - async def get_device_settings(self, device_id: (str | int)): + async def get_device_settings(self, device_id: (str | int), port: int): """Gets the current values of controller specific settings; such as temperature, humidity, and vpd calibration values Args: device_id: The controller id of the settings to grab + port: 0 for controller settings, or the port number for port settings """ 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_SETTING, {"devId": device_id, "port": 0}, headers + API_URL_GET_DEV_SETTING, {"devId": device_id, "port": port}, headers ) return json["data"] - async def update_device_settings( + async def update_advanced_settings( self, device_id: (str | int), - controller_name: str, + port: int, + device_name: str, key_values: list[Tuple[str, int]], ): """Sets a given controller setting to a new value Args: device_id: The device id of the controller to update - controller_name: The current controller name value as it exists in the coordinator from the last refresh call. + port: 0 for controller settings, or the port number for port settings + device_name: The current controller name value as it exists in the coordinator from the last refresh call. key_values: key value pairs of settings to update """ - settings = await self.get_device_settings(device_id) + settings = await self.get_device_settings(device_id, port) # the fetch call does not contain the device name. If we use the payload without setting device name, # the ac infinity api will change the name of the controller to "None". We need to set it first before anything. - settings[ControllerSettingKey.DEV_NAME] = controller_name + settings[AdvancedSettingsKey.DEV_NAME] = device_name # remove fields not expected in the update payload, so we don't get a 400 for key in [ - ControllerSettingKey.SET_ID, - ControllerSettingKey.DEV_MAC_ADDR, - ControllerSettingKey.PORT_RESISTANCE, - ControllerSettingKey.DEV_TIME_ZONE, - ControllerSettingKey.SENSOR_SETTING, - ControllerSettingKey.SENSOR_TRANS_BUFF, - ControllerSettingKey.SUB_DEVICE_VERSION, - ControllerSettingKey.SEC_FUC_REPORT_TIME, - ControllerSettingKey.UPDATE_ALL_PORT, - ControllerSettingKey.CALIBRATION_TIME, + AdvancedSettingsKey.SET_ID, + AdvancedSettingsKey.DEV_MAC_ADDR, + AdvancedSettingsKey.PORT_RESISTANCE, + AdvancedSettingsKey.DEV_TIME_ZONE, + AdvancedSettingsKey.SENSOR_SETTING, + AdvancedSettingsKey.SENSOR_TRANS_BUFF, + AdvancedSettingsKey.SUB_DEVICE_VERSION, + AdvancedSettingsKey.SEC_FUC_REPORT_TIME, + AdvancedSettingsKey.UPDATE_ALL_PORT, + AdvancedSettingsKey.CALIBRATION_TIME, ]: if key in settings: del settings[key] # Find string based fields that are null and set them to empty string. Add any keys that don't exist. for key in [ - ControllerSettingKey.SENSOR_TRANS_BUFF_STR, - ControllerSettingKey.SENSOR_SETTING_STR, - ControllerSettingKey.PORT_PARAM_DATA, - ControllerSettingKey.PARAM_SENSORS, + AdvancedSettingsKey.SENSOR_TRANS_BUFF_STR, + AdvancedSettingsKey.SENSOR_SETTING_STR, + AdvancedSettingsKey.PORT_PARAM_DATA, + AdvancedSettingsKey.PARAM_SENSORS, ]: if key not in settings or settings[key] is None: settings[key] = "" # Add defaulted fields that exist in the update call on the phone app, but may not exist in the fetch call for key in [ - ControllerSettingKey.SENSOR_ONE_TYPE, - ControllerSettingKey.IS_SHARE, - ControllerSettingKey.TARGET_VPD_SWITCH, - ControllerSettingKey.SENSOR_TWO_TYPE, - ControllerSettingKey.ZONE_SENSOR_TYPE, + AdvancedSettingsKey.SENSOR_ONE_TYPE, + AdvancedSettingsKey.IS_SHARE, + AdvancedSettingsKey.TARGET_VPD_SWITCH, + AdvancedSettingsKey.SENSOR_TWO_TYPE, + AdvancedSettingsKey.ZONE_SENSOR_TYPE, ]: if key not in settings: settings[key] = 0 # Convert ids that are strings on the fetch call to int values for the update call - settings[ControllerSettingKey.DEV_ID] = int( - settings[ControllerSettingKey.DEV_ID] - ) + settings[AdvancedSettingsKey.DEV_ID] = int(settings[AdvancedSettingsKey.DEV_ID]) # Set any values that are None to 0 as that's what the update endpoint expects. for key in settings: diff --git a/custom_components/ac_infinity/const.py b/custom_components/ac_infinity/const.py index 8c327eb..a4a5b1a 100644 --- a/custom_components/ac_infinity/const.py +++ b/custom_components/ac_infinity/const.py @@ -44,15 +44,32 @@ class PortPropertyKey: # noinspection SpellCheckingInspection -class ControllerSettingKey: +class AdvancedSettingsKey: # /api/dev/getDevSetting # /api/dev/updateAdvSetting + DEV_ID = "devId" + DEV_NAME = "devName" + + # fields associated with controller advanced settings TEMP_UNIT = "devCompany" CALIBRATE_TEMP = "devCt" CALIBRATE_TEMP_F = "devCth" CALIBRATE_HUMIDITY = "devCh" VPD_LEAF_TEMP_OFFSET = "vpdCt" VPD_LEAF_TEMP_OFFSET_F = "vpdCth" + + # fields associated with port advanced settings + DYNAMIC_RESPONSE_TYPE = "isFlag" + DYNAMIC_TRANSITION_TEMP = "devTt" + DYNAMIC_TRANSITION_TEMP_F = "devTth" + DYNAMIC_TRANSITION_HUMIDITY = "devTh" + DYNAMIC_TRANSITION_VPD = "vpdTransition" + DYNAMIC_BUFFER_TEMP = "devBt" + DYNAMIC_BUFFER_TEMP_F = "devBth" + DYNAMIC_BUFFER_HUMIDITY = "devBh" + DYNAMIC_BUFFER_VPD = "devBvpd" + + # unassociated fields used for cleaning data CALIBRATION_TIME = "calibrationTime" SENSOR_SETTING = "sensorSetting" SENSOR_TRANS_BUFF = "sensorTransBuff" @@ -62,7 +79,6 @@ class ControllerSettingKey: SUPPORT_OTA = "supportOta" SET_ID = "setId" DEV_MAC_ADDR = "devMacAddr" - DEV_NAME = "devName" PORT_RESISTANCE = "portResistance" DEV_TIME_ZONE = "devTimeZone" PORT_PARAM_DATA = "portParamData" @@ -77,11 +93,10 @@ class ControllerSettingKey: SENSOR_TWO_TYPE = "sensorTwoType" PARAM_SENSORS = "paramSensors" ZONE_SENSOR_TYPE = "zoneSensorType" - DEV_ID = "devId" # noinspection SpellCheckingInspection -class PortSettingKey: +class PortControlKey: # /api/dev/getdevModeSettingsList # /api/dev/addDevMode DEV_ID = "devId" diff --git a/custom_components/ac_infinity/core.py b/custom_components/ac_infinity/core.py index cbde9af..e5ef3da 100644 --- a/custom_components/ac_infinity/core.py +++ b/custom_components/ac_infinity/core.py @@ -16,7 +16,14 @@ from custom_components.ac_infinity.client import ACInfinityClient -from .const import DOMAIN, HOST, MANUFACTURER, ControllerPropertyKey, PortPropertyKey +from .const import ( + DOMAIN, + HOST, + MANUFACTURER, + ControllerPropertyKey, + PortControlKey, + PortPropertyKey, +) _LOGGER = logging.getLogger(__name__) @@ -91,6 +98,8 @@ def __get_device_model_by_device_type(device_type: int) -> str: match device_type: case 11: return "UIS Controller 69 Pro (CTR69P)" + case 18: + return "UIS CONTROLLER 69 Pro+ (CTR69Q)" case _: return f"UIS Controller Type {device_type}" @@ -157,11 +166,11 @@ class ACInfinityService: # api/user/devInfoListAll json organized by controller device id and port index _port_properties: dict[Tuple[str, int], Any] = {} - # api/dev/getDevSetting json organized by controller device id - _controller_settings: dict[str, Any] = {} - # api/dev/getDevModeSettingList json organized by controller device id and port index - _port_settings: dict[Tuple[str, int], Any] = {} + _port_controls: dict[Tuple[str, int], Any] = {} + + # api/dev/getDevSetting json organized by controller device id and port (index 0 represents controller settings) + _device_settings: dict[Tuple[str, int], Any] = {} def __init__(self, email: str, password: str) -> None: """ @@ -171,6 +180,24 @@ def __init__(self, email: str, password: str) -> None: """ self._client = ACInfinityClient(HOST, email, password) + def get_controller_property_exists( + self, controller_id: (str | int), property_key: str + ) -> bool: + """returns if a given property exists on a given controller. + + Args: + controller_id: the device id of the controller + property_key: the json field name for the data being retrieved + """ + normalized_id = str(controller_id) + if normalized_id in self._controller_properties: + result = self._controller_properties[normalized_id] + if property_key in result: + return True + return property_key in result[ControllerPropertyKey.DEVICE_INFO] + + return False + def get_controller_property( self, controller_id: (str | int), property_key: str, default_value=None ): @@ -185,7 +212,8 @@ def get_controller_property( if normalized_id in self._controller_properties: result = self._controller_properties[normalized_id] if property_key in result: - return result[property_key] + value = result[property_key] + return value if value is not None else default_value elif property_key in result[ControllerPropertyKey.DEVICE_INFO]: value = result[ControllerPropertyKey.DEVICE_INFO][property_key] return value if value is not None else default_value @@ -203,6 +231,25 @@ def __set_controller_properties_json( """ self._controller_properties[str(controller_id)] = json + def get_port_property_exists( + self, + controller_id: (str | int), + port_index: int, + setting_key: str, + ) -> bool: + """return if a given property key exists on a given device port + + Args: + controller_id: the device id of the controller + port_index: the index of the port on the controller + setting_key: the setting to pull the value of + """ + normalized_id = (str(controller_id), port_index) + return ( + normalized_id in self._port_properties + and setting_key in self._port_properties[normalized_id] + ) + def get_port_property( self, controller_id: (str | int), @@ -239,6 +286,17 @@ def __set_port_properties_json( """ self._port_properties[(controller_id, port_index)] = json + def get_controller_setting_exists( + self, controller_id: (str | int), setting_key: str + ) -> bool: + """returns if a given setting exists on a given controller. + + Args: + controller_id: the device id of the controller + setting_key: the json field name for the data being retrieved + """ + return self.get_port_setting_exists(controller_id, 0, setting_key) + def get_controller_setting( self, controller_id: (str | int), setting_key: str, default_value=None ): @@ -249,25 +307,84 @@ def get_controller_setting( setting_key: the json field name for the data being retrieved default_value: the value to return if the controller or property doesn't exist """ + return self.get_port_setting(controller_id, 0, setting_key, default_value) + + def get_port_setting_exists( + self, controller_id: (str | int), port_index: int, setting_key: str + ) -> bool: + """returns if a given setting exists on a given controller. + + Args: + controller_id: the device id of the controller + port_index: the port index of the device. + setting_key: the json field name for the data being retrieved + """ normalized_id = str(controller_id) - if normalized_id in self._controller_settings: - result = self._controller_settings[normalized_id] + return ( + normalized_id, + port_index, + ) in self._device_settings and setting_key in self._device_settings[ + (normalized_id, port_index) + ] + + def get_port_setting( + self, + controller_id: (str | int), + port_index: int, + setting_key: str, + default_value=None, + ): + """gets a property value for a given device, if both the setting and device exist. + + Args: + controller_id: the device id of the controller + port_index: the port index of the device + setting_key: the json field name for the data being retrieved + default_value: the value to return if the controller or property doesn't exist + """ + normalized_id = str(controller_id) + if (normalized_id, port_index) in self._device_settings: + result = self._device_settings[(normalized_id, port_index)] if setting_key in result: value = result[setting_key] return value if value is not None else default_value return default_value - def __set_controller_settings_json(self, controller_id: str, json: Any) -> None: + def __set_settings_json( + self, controller_id: str, port_index: int, json: Any + ) -> None: """sets the json settings data for a given controller Args: controller_id: the device id of the controller json: he relevant json snippet from the returned API payload """ - self._controller_settings[controller_id] = json + self._device_settings[(controller_id, port_index)] = json - def get_port_setting( + def get_port_control_exists( + self, + controller_id: (str | int), + port_index: int, + setting_key: str, + ) -> bool: + """return if a given setting key exists on a given device port + + Args: + controller_id: the device id of the controller + port_index: the index of the port on the controller + setting_key: the setting to pull the value of + """ + normalized_id = (str(controller_id), port_index) + if normalized_id in self._port_controls: + found = self._port_controls[normalized_id] + if setting_key in found: + return True + return setting_key in found[PortControlKey.DEV_SETTING] + + return False + + def get_port_control( self, controller_id: (str | int), port_index: int, @@ -283,15 +400,18 @@ def get_port_setting( default_value: the default value to return if the controller, port, or setting doesn't exist """ normalized_id = (str(controller_id), port_index) - if normalized_id in self._port_settings: - found = self._port_settings[normalized_id] - if setting_key in found: - value = found[setting_key] + if normalized_id in self._port_controls: + result = self._port_controls[normalized_id] + if setting_key in result: + value = result[setting_key] + return value if value is not None else default_value + elif setting_key in result[PortControlKey.DEV_SETTING]: + value = result[PortControlKey.DEV_SETTING][setting_key] return value if value is not None else default_value return default_value - def __set_port_settings_json( + def __set_port_controls_json( self, controller_id: str, port_index: int, json: Any ) -> None: """sets the json setting data for a given controller and port @@ -301,7 +421,7 @@ def __set_port_settings_json( port_index: the index of the port on the controller json: the relevant json snippet from the returned API payload """ - self._port_settings[(controller_id, port_index)] = json + self._port_controls[(controller_id, port_index)] = json async def refresh(self) -> None: """refreshes the values of properties and settings from the AC infinity API""" @@ -324,11 +444,9 @@ async def refresh(self) -> None: # retrieve and set controller settings; temperature, humidity, and vpd offsets controller_settings_json = await self._client.get_device_settings( - controller_id - ) - self.__set_controller_settings_json( - controller_id, controller_settings_json + controller_id, 0 ) + self.__set_settings_json(controller_id, 0, controller_settings_json) for port_properties_json in controller_properties_json[ ControllerPropertyKey.DEVICE_INFO @@ -340,13 +458,21 @@ async def refresh(self) -> None: controller_id, port_index, port_properties_json ) - # retrieve and set port settings; current mode, temperature triggers, on/off speed, etc... - port_settings_json = ( + # retrieve and set port controls; current mode, temperature triggers, on/off speed, etc... + port_controls_json = ( await self._client.get_device_mode_settings_list( controller_id, port_index ) ) - self.__set_port_settings_json( + self.__set_port_controls_json( + controller_id, port_index, port_controls_json + ) + + # retrieve and set port settings; Dynamic Response, Transition values, Buffer values, etc.. + port_settings_json = await self._client.get_device_settings( + controller_id, port_index + ) + self.__set_settings_json( controller_id, port_index, port_settings_json ) return # update successful. eject from the infinite while loop. @@ -391,24 +517,78 @@ async def update_controller_setting( await self.update_controller_settings(controller_id, [(setting_key, new_value)]) async def update_controller_settings( + self, controller_id: (str | int), key_values: list[Tuple[str, int]] + ): + """Update the values of a set of settings via the AC Infinity API + + Args: + controller_id: The device id of the controller to update + key_values: a list of key/value pairs to update, as a tuple of (setting_key, new_value) + """ + device_name = self.get_controller_property( + controller_id, ControllerPropertyKey.DEVICE_NAME + ) + await self.__update_advanced_settings(controller_id, 0, device_name, key_values) + + async def update_port_setting( + self, + controller_id: (str | int), + port_index: int, + setting_key: str, + new_value: int, + ): + """Update the value of a setting via the AC Infinity API + + Args: + controller_id: the device id of the controller + port_index: the port of the device + setting_key: the setting to update the value of + new_value: the new value of the setting to set + """ + await self.update_port_settings( + controller_id, port_index, [(setting_key, new_value)] + ) + + async def update_port_settings( + self, + controller_id: (str | int), + port_index: int, + key_values: list[Tuple[str, int]], + ): + """Update the values of a set of settings via the AC Infinity API + + Args: + controller_id: The device id of the controller to update + port_index: the port of the device + key_values: a list of key/value pairs to update, as a tuple of (setting_key, new_value) + """ + device_name = self.get_port_property( + controller_id, port_index, PortPropertyKey.NAME + ) + await self.__update_advanced_settings( + controller_id, port_index, device_name, key_values + ) + + async def __update_advanced_settings( self, controller_id: (str | int), + port: int, + device_name: str, key_values: list[Tuple[str, int]], ): """Update the values of a set of settings via the AC Infinity API Args: - controller_id: the device id of the controller + controller_id: The device id of the controller to update + port: 0 for controller settings, or the port number for port settings + device_name: The current controller name value as it exists in the coordinator from the last refresh call. key_values: a list of key/value pairs to update, as a tuple of (setting_key, new_value) """ try_count = 0 while True: try: - device_name = self.get_controller_property( - controller_id, ControllerPropertyKey.DEVICE_NAME - ) - await self._client.update_device_settings( - controller_id, device_name, key_values + await self._client.update_advanced_settings( + controller_id, port, device_name, key_values ) return except BaseException as ex: @@ -426,7 +606,7 @@ async def update_controller_settings( ) raise - async def update_port_setting( + async def update_port_control( self, controller_id: (str | int), port_index: int, @@ -441,11 +621,11 @@ async def update_port_setting( setting_key: the setting to update the value of new_value: the new value of the setting to set """ - await self.update_port_settings( + await self.update_port_controls( controller_id, port_index, [(setting_key, new_value)] ) - async def update_port_settings( + async def update_port_controls( self, controller_id: (str | int), port_index: int, @@ -514,12 +694,11 @@ class ACInfinityEntity(CoordinatorEntity[ACInfinityDataUpdateCoordinator]): _attr_has_entity_name = True def __init__( - self, - coordinator: ACInfinityDataUpdateCoordinator, - data_key: str, + self, coordinator: ACInfinityDataUpdateCoordinator, data_key: str, platform: str ): super().__init__(coordinator) self._data_key = data_key + self._platform_name = platform @property def ac_infinity(self) -> ACInfinityService: @@ -536,16 +715,28 @@ def unique_id(self) -> str: def device_info(self) -> DeviceInfo: """Returns the device info for the controller entity""" + @property + @abstractmethod + def is_suitable(self) -> bool: + """Returns true if the field's backing key exists in the initial data obtained""" + + @property + def platform_name(self) -> str: + return self._platform_name + class ACInfinityControllerEntity(ACInfinityEntity): def __init__( self, coordinator: ACInfinityDataUpdateCoordinator, controller: ACInfinityController, + suitable_fn: Callable[[ACInfinityEntity, ACInfinityController], bool], data_key: str, + platform: str, ): - super().__init__(coordinator, data_key) + super().__init__(coordinator, data_key, platform) self._controller = controller + self._suitable_fn = suitable_fn @property def unique_id(self) -> str: @@ -561,16 +752,23 @@ def device_info(self) -> DeviceInfo: def controller(self) -> ACInfinityController: return self._controller + @property + def is_suitable(self) -> bool: + return self._suitable_fn(self, self.controller) + class ACInfinityPortEntity(ACInfinityEntity): def __init__( self, coordinator: ACInfinityDataUpdateCoordinator, port: ACInfinityPort, + suitable_fn: Callable[[ACInfinityEntity, ACInfinityPort], bool], data_key: str, + platform: str, ): - super().__init__(coordinator, data_key) + super().__init__(coordinator, data_key, platform) self._port = port + self._suitable_fn = suitable_fn @property def unique_id(self) -> str: @@ -586,11 +784,17 @@ def device_info(self) -> DeviceInfo: def port(self) -> ACInfinityPort: return self._port + @property + def is_suitable(self) -> bool: + return self._suitable_fn(self, self.port) + @dataclass class ACInfinityControllerReadOnlyMixin: """Mixin for retrieving values for controller level sensors""" + suitable_fn: Callable[[ACInfinityEntity, ACInfinityController], bool] + """Input data object and a device id; output if suitable""" get_value_fn: Callable[[ACInfinityEntity, ACInfinityController], StateType] """Input data object and a device id; output the value.""" @@ -609,6 +813,8 @@ class ACInfinityControllerReadWriteMixin(ACInfinityControllerReadOnlyMixin): class ACInfinityPortReadOnlyMixin: """Mixin for retrieving values for port device level sensors""" + suitable_fn: Callable[[ACInfinityEntity, ACInfinityPort], bool] + """Input data object, device id, and port number; output if suitable.""" get_value_fn: Callable[[ACInfinityEntity, ACInfinityPort], StateType] """Input data object, device id, and port number; output the value.""" @@ -623,12 +829,34 @@ class ACInfinityPortReadWriteMixin(ACInfinityPortReadOnlyMixin): """Input data object, device id, port number, and desired value.""" +def suitable_fn_controller_property_default( + entity: ACInfinityEntity, controller: ACInfinityController +): + return entity.ac_infinity.get_controller_property_exists( + controller.device_id, entity.entity_description.key + ) + + +def suitable_fn_port_property_default(entity: ACInfinityEntity, port: ACInfinityPort): + return entity.ac_infinity.get_port_property_exists( + port.controller.device_id, port.port_index, entity.entity_description.key + ) + + def get_value_fn_port_property_default(entity: ACInfinityEntity, port: ACInfinityPort): return entity.ac_infinity.get_port_property( port.controller.device_id, port.port_index, entity.entity_description.key ) +def suitable_fn_controller_setting_default( + entity: ACInfinityEntity, controller: ACInfinityController +): + return entity.ac_infinity.get_controller_setting_exists( + controller.device_id, entity.entity_description.key + ) + + def get_value_fn_controller_setting_default( entity: ACInfinityEntity, controller: ACInfinityController ): @@ -645,6 +873,32 @@ def set_value_fn_controller_setting_default( ) +def suitable_fn_port_control_default(entity: ACInfinityEntity, port: ACInfinityPort): + return entity.ac_infinity.get_port_control_exists( + port.controller.device_id, port.port_index, entity.entity_description.key + ) + + +def get_value_fn_port_control_default(entity: ACInfinityEntity, port: ACInfinityPort): + return entity.ac_infinity.get_port_control( + port.controller.device_id, port.port_index, entity.entity_description.key + ) + + +def set_value_fn_port_control_default( + entity: ACInfinityEntity, port: ACInfinityPort, value: int +): + return entity.ac_infinity.update_port_control( + port.controller.device_id, port.port_index, entity.entity_description.key, value + ) + + +def suitable_fn_port_setting_default(entity: ACInfinityEntity, port: ACInfinityPort): + return entity.ac_infinity.get_port_setting_exists( + port.controller.device_id, port.port_index, entity.entity_description.key + ) + + def get_value_fn_port_setting_default(entity: ACInfinityEntity, port: ACInfinityPort): return entity.ac_infinity.get_port_setting( port.controller.device_id, port.port_index, entity.entity_description.key @@ -657,3 +911,22 @@ def set_value_fn_port_setting_default( return entity.ac_infinity.update_port_setting( port.controller.device_id, port.port_index, entity.entity_description.key, value ) + + +class ACInfinityEntities(list[ACInfinityEntity]): + def append_if_suitable(self, entity: ACInfinityEntity): + if entity.is_suitable: + self.append(entity) + _LOGGER.info( + 'Initializing entity "%s" (%s) for platform "%s".', + entity.unique_id, + entity.translation_key, + entity.platform_name, + ) + else: + _LOGGER.warning( + 'Ignoring unsuitable entity "%s" (%s) for platform "%s".', + entity.unique_id, + entity.translation_key, + entity.platform_name, + ) diff --git a/custom_components/ac_infinity/manifest.json b/custom_components/ac_infinity/manifest.json index 452129c..dcf8c59 100644 --- a/custom_components/ac_infinity/manifest.json +++ b/custom_components/ac_infinity/manifest.json @@ -10,5 +10,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/dalinicus/homeassistant-acinfinity", "requirements": [], - "version": "1.5.0" + "version": "1.6.0" } diff --git a/custom_components/ac_infinity/number.py b/custom_components/ac_infinity/number.py index 9142900..ca7d5fa 100644 --- a/custom_components/ac_infinity/number.py +++ b/custom_components/ac_infinity/number.py @@ -1,4 +1,5 @@ import logging +import math from dataclasses import dataclass from homeassistant.components.number import ( @@ -13,22 +14,28 @@ from custom_components.ac_infinity.const import ( DOMAIN, - ControllerSettingKey, - PortSettingKey, + AdvancedSettingsKey, + PortControlKey, ) from custom_components.ac_infinity.core import ( ACInfinityController, ACInfinityControllerEntity, ACInfinityControllerReadWriteMixin, ACInfinityDataUpdateCoordinator, + ACInfinityEntities, ACInfinityEntity, ACInfinityPort, ACInfinityPortEntity, ACInfinityPortReadWriteMixin, get_value_fn_controller_setting_default, + get_value_fn_port_control_default, get_value_fn_port_setting_default, set_value_fn_controller_setting_default, + set_value_fn_port_control_default, set_value_fn_port_setting_default, + suitable_fn_controller_setting_default, + suitable_fn_port_control_default, + suitable_fn_port_setting_default, ) _LOGGER = logging.getLogger(__name__) @@ -65,13 +72,13 @@ class ACInfinityPortNumberEntityDescription( def __get_value_fn_cal_temp(entity: ACInfinityEntity, controller: ACInfinityController): temp_unit = entity.ac_infinity.get_controller_setting( - controller.device_id, ControllerSettingKey.TEMP_UNIT + controller.device_id, AdvancedSettingsKey.TEMP_UNIT ) return entity.ac_infinity.get_controller_setting( controller.device_id, - ControllerSettingKey.CALIBRATE_TEMP + AdvancedSettingsKey.CALIBRATE_TEMP if temp_unit > 0 - else ControllerSettingKey.CALIBRATE_TEMP_F, + else AdvancedSettingsKey.CALIBRATE_TEMP_F, ) @@ -79,7 +86,7 @@ def __set_value_fn_cal_temp( entity: ACInfinityEntity, controller: ACInfinityController, value: int ): temp_unit = entity.ac_infinity.get_controller_setting( - controller.device_id, ControllerSettingKey.TEMP_UNIT + controller.device_id, AdvancedSettingsKey.TEMP_UNIT ) # in the event that the user swaps from F to C in the ac infinity app without reloading homeassistant, @@ -92,13 +99,13 @@ def __set_value_fn_cal_temp( return entity.ac_infinity.update_controller_settings( controller.device_id, [ - (ControllerSettingKey.CALIBRATE_TEMP, value), - (ControllerSettingKey.CALIBRATE_TEMP_F, 0), + (AdvancedSettingsKey.CALIBRATE_TEMP, value), + (AdvancedSettingsKey.CALIBRATE_TEMP_F, 0), ] if temp_unit > 0 else [ - (ControllerSettingKey.CALIBRATE_TEMP, 0), - (ControllerSettingKey.CALIBRATE_TEMP_F, value), + (AdvancedSettingsKey.CALIBRATE_TEMP, 0), + (AdvancedSettingsKey.CALIBRATE_TEMP_F, value), ], ) @@ -107,13 +114,13 @@ def __get_value_fn_vpd_leaf_temp_offset( entity: ACInfinityEntity, controller: ACInfinityController ): temp_unit = entity.ac_infinity.get_controller_setting( - controller.device_id, ControllerSettingKey.TEMP_UNIT + controller.device_id, AdvancedSettingsKey.TEMP_UNIT ) return entity.ac_infinity.get_controller_setting( controller.device_id, - ControllerSettingKey.VPD_LEAF_TEMP_OFFSET + AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET if temp_unit > 0 - else ControllerSettingKey.VPD_LEAF_TEMP_OFFSET_F, + else AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET_F, ) @@ -121,7 +128,7 @@ def __set_value_fn_vpd_leaf_temp_offset( entity: ACInfinityEntity, controller: ACInfinityController, value: int ): temp_unit = entity.ac_infinity.get_controller_setting( - controller.device_id, ControllerSettingKey.TEMP_UNIT + controller.device_id, AdvancedSettingsKey.TEMP_UNIT ) # in the event that the user swaps from F to C in the ac infinity app without reloading homeassistant, @@ -133,9 +140,9 @@ def __set_value_fn_vpd_leaf_temp_offset( return entity.ac_infinity.update_controller_setting( controller.device_id, - ControllerSettingKey.VPD_LEAF_TEMP_OFFSET + AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET if temp_unit > 0 - else ControllerSettingKey.VPD_LEAF_TEMP_OFFSET_F, + else AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET_F, value, ) @@ -143,7 +150,7 @@ def __set_value_fn_vpd_leaf_temp_offset( def __get_value_fn_timer_duration(entity: ACInfinityEntity, port: ACInfinityPort): # value configured as minutes but stored as seconds return ( - entity.ac_infinity.get_port_setting( + entity.ac_infinity.get_port_control( port.controller.device_id, port.port_index, entity.entity_description.key ) / 60 @@ -154,7 +161,7 @@ def __set_value_fn_timer_duration( entity: ACInfinityEntity, port: ACInfinityPort, value: int ): # value configured as minutes but stored as seconds - return entity.ac_infinity.update_port_setting( + return entity.ac_infinity.update_port_control( port.controller.device_id, port.port_index, entity.entity_description.key, @@ -162,7 +169,29 @@ def __set_value_fn_timer_duration( ) -def __get_value_fn_vpd(entity: ACInfinityEntity, port: ACInfinityPort): +def __get_value_fn_vpd_control(entity: ACInfinityEntity, port: ACInfinityPort): + # value configured as percent (10.2%) but stored as tenths of a percent (102) + return ( + entity.ac_infinity.get_port_control( + port.controller.device_id, port.port_index, entity.entity_description.key + ) + / 10 + ) + + +def __set_value_fn_vpd_control( + entity: ACInfinityEntity, port: ACInfinityPort, value: int +): + # value configured as percent (10.2%) but stored as tenths of a percent (102) + return entity.ac_infinity.update_port_control( + port.controller.device_id, + port.port_index, + entity.entity_description.key, + value * 10, + ) + + +def __get_value_fn_vpd_setting(entity: ACInfinityEntity, port: ACInfinityPort): # value configured as percent (10.2%) but stored as tenths of a percent (102) return ( entity.ac_infinity.get_port_setting( @@ -172,7 +201,9 @@ def __get_value_fn_vpd(entity: ACInfinityEntity, port: ACInfinityPort): ) -def __set_value_fn_vpd(entity: ACInfinityEntity, port: ACInfinityPort, value: int): +def __set_value_fn_vpd_setting( + entity: ACInfinityEntity, port: ACInfinityPort, value: int +): # value configured as percent (10.2%) but stored as tenths of a percent (102) return entity.ac_infinity.update_port_setting( port.controller.device_id, @@ -185,14 +216,14 @@ def __set_value_fn_vpd(entity: ACInfinityEntity, port: ACInfinityPort, value: in def __set_value_fn_temp_auto_low( entity: ACInfinityEntity, port: ACInfinityPort, value: int ): - return entity.ac_infinity.update_port_settings( + return entity.ac_infinity.update_port_controls( port.controller.device_id, port.port_index, [ # value is received from HA as C - (PortSettingKey.AUTO_TEMP_LOW_TRIGGER, value), + (PortControlKey.AUTO_TEMP_LOW_TRIGGER, value), # degrees F must be calculated and set in addition to C - (PortSettingKey.AUTO_TEMP_LOW_TRIGGER_F, int(round((value * 1.8) + 32, 0))), + (PortControlKey.AUTO_TEMP_LOW_TRIGGER_F, int(round((value * 1.8) + 32, 0))), ], ) @@ -200,24 +231,108 @@ def __set_value_fn_temp_auto_low( def __set_value_fn_temp_auto_high( entity: ACInfinityEntity, port: ACInfinityPort, value: int ): - return entity.ac_infinity.update_port_settings( + return entity.ac_infinity.update_port_controls( port.controller.device_id, port.port_index, [ # value is received from HA as C - (PortSettingKey.AUTO_TEMP_HIGH_TRIGGER, value), + (PortControlKey.AUTO_TEMP_HIGH_TRIGGER, value), # degrees F must be calculated and set in addition to C ( - PortSettingKey.AUTO_TEMP_HIGH_TRIGGER_F, + PortControlKey.AUTO_TEMP_HIGH_TRIGGER_F, int(round((value * 1.8) + 32, 0)), ), ], ) +def __get_value_fn_dynamic_transition_temp( + entity: ACInfinityEntity, port: ACInfinityPort +): + temp_unit = entity.ac_infinity.get_controller_setting( + port.controller.device_id, AdvancedSettingsKey.TEMP_UNIT + ) + + return entity.ac_infinity.get_port_setting( + port.controller.device_id, + port.port_index, + AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP + if temp_unit > 0 + else AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP_F, + ) + + +def __get_value_fn_dynamic_buffer_temp(entity: ACInfinityEntity, port: ACInfinityPort): + temp_unit = entity.ac_infinity.get_controller_setting( + port.controller.device_id, AdvancedSettingsKey.TEMP_UNIT + ) + + return entity.ac_infinity.get_port_setting( + port.controller.device_id, + port.port_index, + AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP + if temp_unit > 0 + else AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP_F, + ) + + +def __set_value_fn_dynamic_transition_temp( + entity: ACInfinityEntity, port: ACInfinityPort, value: int +): + temp_unit = entity.ac_infinity.get_controller_setting( + port.controller.device_id, AdvancedSettingsKey.TEMP_UNIT + ) + + # in the event that the user swaps from F to C in the ac infinity app without reloading homeassistant, + # we need to put bounds on the value since the entity max values will still be 20 instead of 10 + if temp_unit > 0 and value > 10: + value = 10 + + return entity.ac_infinity.update_port_settings( + port.controller.device_id, + port.port_index, + [ + (AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP, value), + (AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP_F, value * 2), + ] + if temp_unit > 0 + else [ + (AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP, math.floor(value / 2)), + (AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP_F, value), + ], + ) + + +def __set_value_fn_dynamic_buffer_temp( + entity: ACInfinityEntity, port: ACInfinityPort, value: int +): + temp_unit = entity.ac_infinity.get_controller_setting( + port.controller.device_id, AdvancedSettingsKey.TEMP_UNIT + ) + + # in the event that the user swaps from F to C in the ac infinity app without reloading homeassistant, + # we need to put bounds on the value since the entity max values will still be 20 instead of 10 + if temp_unit > 0 and value > 10: + value = 10 + + return entity.ac_infinity.update_port_settings( + port.controller.device_id, + port.port_index, + [ + (AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP, value), + (AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP_F, value * 2), + ] + if temp_unit > 0 + else [ + (AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP, math.floor(value / 2)), + (AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP_F, value), + ], + ) + + CONTROLLER_DESCRIPTIONS: list[ACInfinityControllerNumberEntityDescription] = [ ACInfinityControllerNumberEntityDescription( - key=ControllerSettingKey.CALIBRATE_TEMP, + key=AdvancedSettingsKey.CALIBRATE_TEMP, device_class=None, mode=NumberMode.AUTO, native_min_value=-20, @@ -226,11 +341,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:thermometer-plus", translation_key="temperature_calibration", native_unit_of_measurement=None, + suitable_fn=suitable_fn_controller_setting_default, get_value_fn=__get_value_fn_cal_temp, set_value_fn=__set_value_fn_cal_temp, ), ACInfinityControllerNumberEntityDescription( - key=ControllerSettingKey.CALIBRATE_HUMIDITY, + key=AdvancedSettingsKey.CALIBRATE_HUMIDITY, device_class=None, mode=NumberMode.AUTO, native_min_value=-10, @@ -239,11 +355,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:cloud-percent-outline", translation_key="humidity_calibration", native_unit_of_measurement=None, + suitable_fn=suitable_fn_controller_setting_default, get_value_fn=get_value_fn_controller_setting_default, set_value_fn=set_value_fn_controller_setting_default, ), ACInfinityControllerNumberEntityDescription( - key=ControllerSettingKey.VPD_LEAF_TEMP_OFFSET, + key=AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET, device_class=None, mode=NumberMode.AUTO, native_min_value=-20, @@ -252,6 +369,7 @@ def __set_value_fn_temp_auto_high( icon="mdi:leaf", translation_key="vpd_leaf_temperature_offset", native_unit_of_measurement=None, + suitable_fn=suitable_fn_controller_setting_default, get_value_fn=__get_value_fn_vpd_leaf_temp_offset, set_value_fn=__set_value_fn_vpd_leaf_temp_offset, ), @@ -259,7 +377,7 @@ def __set_value_fn_temp_auto_high( PORT_DESCRIPTIONS: list[ACInfinityPortNumberEntityDescription] = [ ACInfinityPortNumberEntityDescription( - key=PortSettingKey.ON_SPEED, + key=PortControlKey.ON_SPEED, device_class=NumberDeviceClass.POWER_FACTOR, mode=NumberMode.AUTO, native_min_value=0, @@ -268,11 +386,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:knob", translation_key="on_power", native_unit_of_measurement=None, - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.OFF_SPEED, + key=PortControlKey.OFF_SPEED, device_class=NumberDeviceClass.POWER_FACTOR, mode=NumberMode.AUTO, native_min_value=0, @@ -281,11 +400,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:knob", translation_key="off_power", native_unit_of_measurement=None, - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.TIMER_DURATION_TO_ON, + key=PortControlKey.TIMER_DURATION_TO_ON, device_class=NumberDeviceClass.DURATION, mode=NumberMode.BOX, native_min_value=0, @@ -294,11 +414,12 @@ def __set_value_fn_temp_auto_high( icon=None, # default translation_key="timer_mode_minutes_to_on", native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_timer_duration, set_value_fn=__set_value_fn_timer_duration, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.TIMER_DURATION_TO_OFF, + key=PortControlKey.TIMER_DURATION_TO_OFF, device_class=NumberDeviceClass.DURATION, mode=NumberMode.BOX, native_min_value=0, @@ -307,11 +428,12 @@ def __set_value_fn_temp_auto_high( icon=None, # default translation_key="timer_mode_minutes_to_off", native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_timer_duration, set_value_fn=__set_value_fn_timer_duration, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.CYCLE_DURATION_ON, + key=PortControlKey.CYCLE_DURATION_ON, device_class=NumberDeviceClass.DURATION, mode=NumberMode.BOX, native_min_value=0, @@ -320,11 +442,12 @@ def __set_value_fn_temp_auto_high( icon=None, # default translation_key="cycle_mode_minutes_on", native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_timer_duration, set_value_fn=__set_value_fn_timer_duration, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.CYCLE_DURATION_OFF, + key=PortControlKey.CYCLE_DURATION_OFF, device_class=NumberDeviceClass.DURATION, mode=NumberMode.BOX, native_min_value=0, @@ -333,11 +456,12 @@ def __set_value_fn_temp_auto_high( icon=None, # default translation_key="cycle_mode_minutes_off", native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_timer_duration, set_value_fn=__set_value_fn_timer_duration, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.VPD_LOW_TRIGGER, + key=PortControlKey.VPD_LOW_TRIGGER, device_class=NumberDeviceClass.PRESSURE, mode=NumberMode.BOX, native_min_value=0, @@ -346,11 +470,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:water-thermometer-outline", translation_key="vpd_mode_low_trigger", native_unit_of_measurement=None, - get_value_fn=__get_value_fn_vpd, - set_value_fn=__set_value_fn_vpd, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=__get_value_fn_vpd_control, + set_value_fn=__set_value_fn_vpd_control, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.VPD_HIGH_TRIGGER, + key=PortControlKey.VPD_HIGH_TRIGGER, device_class=NumberDeviceClass.PRESSURE, mode=NumberMode.BOX, native_min_value=0, @@ -359,11 +484,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:water-thermometer-outline", translation_key="vpd_mode_high_trigger", native_unit_of_measurement=None, - get_value_fn=__get_value_fn_vpd, - set_value_fn=__set_value_fn_vpd, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=__get_value_fn_vpd_control, + set_value_fn=__set_value_fn_vpd_control, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.AUTO_HUMIDITY_LOW_TRIGGER, + key=PortControlKey.AUTO_HUMIDITY_LOW_TRIGGER, device_class=NumberDeviceClass.HUMIDITY, mode=NumberMode.AUTO, native_min_value=0, @@ -372,11 +498,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:water-percent", translation_key="auto_mode_humidity_low_trigger", native_unit_of_measurement=None, - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.AUTO_HUMIDITY_HIGH_TRIGGER, + key=PortControlKey.AUTO_HUMIDITY_HIGH_TRIGGER, device_class=NumberDeviceClass.HUMIDITY, mode=NumberMode.AUTO, native_min_value=0, @@ -385,11 +512,12 @@ def __set_value_fn_temp_auto_high( icon="mdi:water-percent", translation_key="auto_mode_humidity_high_trigger", native_unit_of_measurement=None, - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.AUTO_TEMP_LOW_TRIGGER, + key=PortControlKey.AUTO_TEMP_LOW_TRIGGER, device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, mode=NumberMode.AUTO, @@ -398,11 +526,12 @@ def __set_value_fn_temp_auto_high( native_step=1, icon=None, translation_key="auto_mode_temp_low_trigger", - get_value_fn=get_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, set_value_fn=__set_value_fn_temp_auto_low, ), ACInfinityPortNumberEntityDescription( - key=PortSettingKey.AUTO_TEMP_HIGH_TRIGGER, + key=PortControlKey.AUTO_TEMP_HIGH_TRIGGER, device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, mode=NumberMode.AUTO, @@ -411,9 +540,94 @@ def __set_value_fn_temp_auto_high( native_step=1, icon=None, translation_key="auto_mode_temp_high_trigger", - get_value_fn=get_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, set_value_fn=__set_value_fn_temp_auto_high, ), + ACInfinityPortNumberEntityDescription( + key=AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP, + device_class=None, + mode=NumberMode.AUTO, + native_min_value=0, + native_max_value=20, + native_step=1, + icon="mdi:thermometer-plus", + translation_key="dynamic_transition_temp", + native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_setting_default, + get_value_fn=__get_value_fn_dynamic_transition_temp, + set_value_fn=__set_value_fn_dynamic_transition_temp, + ), + ACInfinityPortNumberEntityDescription( + key=AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, + device_class=None, + mode=NumberMode.AUTO, + native_min_value=0, + native_max_value=10, + native_step=1, + icon="mdi:cloud-percent-outline", + translation_key="dynamic_transition_humidity", + native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_setting_default, + get_value_fn=get_value_fn_port_setting_default, + set_value_fn=set_value_fn_port_setting_default, + ), + ACInfinityPortNumberEntityDescription( + key=AdvancedSettingsKey.DYNAMIC_TRANSITION_VPD, + device_class=None, + mode=NumberMode.AUTO, + native_min_value=0, + native_max_value=1, + native_step=0.1, + icon="mdi:leaf", + translation_key="dynamic_transition_vpd", + native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_setting_default, + get_value_fn=__get_value_fn_vpd_setting, + set_value_fn=__set_value_fn_vpd_setting, + ), + ACInfinityPortNumberEntityDescription( + key=AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP, + device_class=None, + mode=NumberMode.AUTO, + native_min_value=0, + native_max_value=20, + native_step=1, + icon="mdi:thermometer-plus", + translation_key="dynamic_buffer_temp", + native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_setting_default, + get_value_fn=__get_value_fn_dynamic_buffer_temp, + set_value_fn=__set_value_fn_dynamic_buffer_temp, + ), + ACInfinityPortNumberEntityDescription( + key=AdvancedSettingsKey.DYNAMIC_BUFFER_HUMIDITY, + device_class=None, + mode=NumberMode.AUTO, + native_min_value=0, + native_max_value=10, + native_step=1, + icon="mdi:cloud-percent-outline", + translation_key="dynamic_buffer_humidity", + native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_setting_default, + get_value_fn=get_value_fn_port_setting_default, + set_value_fn=set_value_fn_port_setting_default, + ), + ACInfinityPortNumberEntityDescription( + key=AdvancedSettingsKey.DYNAMIC_BUFFER_VPD, + device_class=None, + mode=NumberMode.AUTO, + native_min_value=0, + native_max_value=1, + native_step=0.1, + icon="mdi:leaf", + translation_key="dynamic_buffer_vpd", + native_unit_of_measurement=None, + suitable_fn=suitable_fn_port_setting_default, + get_value_fn=__get_value_fn_vpd_setting, + set_value_fn=__set_value_fn_vpd_setting, + ), ] @@ -426,7 +640,13 @@ def __init__( description: ACInfinityControllerNumberEntityDescription, controller: ACInfinityController, ) -> None: - super().__init__(coordinator, controller, description.key) + super().__init__( + coordinator, + controller, + description.suitable_fn, + description.key, + Platform.NUMBER, + ) self.entity_description = description @property @@ -450,7 +670,9 @@ def __init__( description: ACInfinityPortNumberEntityDescription, port: ACInfinityPort, ) -> None: - super().__init__(coordinator, port, description.key) + super().__init__( + coordinator, port, description.suitable_fn, description.key, Platform.NUMBER + ) self.entity_description = description @property @@ -473,37 +695,36 @@ async def async_setup_entry( coordinator: ACInfinityDataUpdateCoordinator = hass.data[DOMAIN][config.entry_id] controllers = coordinator.ac_infinity.get_all_controller_properties() - entities = [] + entities = ACInfinityEntities() for controller in controllers: temp_unit = coordinator.ac_infinity.get_controller_setting( - controller.device_id, ControllerSettingKey.TEMP_UNIT + controller.device_id, AdvancedSettingsKey.TEMP_UNIT ) for description in CONTROLLER_DESCRIPTIONS: entity = ACInfinityControllerNumberEntity( coordinator, description, controller ) - if temp_unit > 0 and ( - description.key == ControllerSettingKey.CALIBRATE_TEMP - or ControllerSettingKey.VPD_LEAF_TEMP_OFFSET + + if temp_unit > 0 and description.key in ( + AdvancedSettingsKey.CALIBRATE_TEMP, + AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET, ): - # Celsius is restricted to ±10C versus Fahrenheit which is restricted to ±20C + # Celsius is restricted to ±10C versus Fahrenheit which is restricted to ±20F entity.entity_description.native_min_value = -10 entity.entity_description.native_max_value = 10 - entities.append(entity) - _LOGGER.info( - 'Initializing entity "%s" for platform "%s".', - entity.unique_id, - Platform.NUMBER, - ) + entities.append_if_suitable(entity) + for port in controller.ports: for description in PORT_DESCRIPTIONS: entity = ACInfinityPortNumberEntity(coordinator, description, port) - entities.append(entity) - _LOGGER.info( - 'Initializing entity "%s" for platform "%s".', - entity.unique_id, - Platform.NUMBER, - ) + if temp_unit > 0 and description.key in ( + AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP, + AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP, + ): + # Celsius max value is 10C versus Fahrenheit which maxes out at 20F + entity.entity_description.native_max_value = 10 + + entities.append_if_suitable(entity) add_entities_callback(entities) diff --git a/custom_components/ac_infinity/select.py b/custom_components/ac_infinity/select.py index d6cf135..89c8de4 100644 --- a/custom_components/ac_infinity/select.py +++ b/custom_components/ac_infinity/select.py @@ -6,13 +6,20 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from custom_components.ac_infinity.const import DOMAIN, PortSettingKey +from custom_components.ac_infinity.const import ( + DOMAIN, + AdvancedSettingsKey, + PortControlKey, +) from custom_components.ac_infinity.core import ( ACInfinityDataUpdateCoordinator, + ACInfinityEntities, ACInfinityEntity, ACInfinityPort, ACInfinityPortEntity, ACInfinityPortReadWriteMixin, + suitable_fn_port_control_default, + suitable_fn_port_setting_default, ) _LOGGER = logging.getLogger(__name__) @@ -45,12 +52,14 @@ class ACInfinityPortSelectEntityDescription( "VPD", ] +DYNAMIC_RESPONSE_OPTIONS = ["Transition", "Buffer"] + def __get_value_fn_active_mode(entity: ACInfinityEntity, port: ACInfinityPort): return MODE_OPTIONS[ # data is 1 based. Adjust to 0 based enum by subtracting 1 - entity.ac_infinity.get_port_setting( - port.controller.device_id, port.port_index, PortSettingKey.AT_TYPE + entity.ac_infinity.get_port_control( + port.controller.device_id, port.port_index, PortControlKey.AT_TYPE ) - 1 ] @@ -59,23 +68,55 @@ def __get_value_fn_active_mode(entity: ACInfinityEntity, port: ACInfinityPort): def __set_value_fn_active_mode( entity: ACInfinityEntity, port: ACInfinityPort, value: str ): - return entity.ac_infinity.update_port_setting( + return entity.ac_infinity.update_port_control( port.controller.device_id, port.port_index, - PortSettingKey.AT_TYPE, + PortControlKey.AT_TYPE, # data is 1 based. Adjust from 0 based enum by adding 1 MODE_OPTIONS.index(value) + 1, ) +def __get_value_fn_dynamic_response_type( + entity: ACInfinityEntity, port: ACInfinityPort +): + return DYNAMIC_RESPONSE_OPTIONS[ + entity.ac_infinity.get_port_setting( + port.controller.device_id, + port.port_index, + AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, + ) + ] + + +def __set_value_fn_dynamic_response_type( + entity: ACInfinityEntity, port: ACInfinityPort, value: str +): + return entity.ac_infinity.update_port_setting( + port.controller.device_id, + port.port_index, + AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, + DYNAMIC_RESPONSE_OPTIONS.index(value), + ) + + PORT_DESCRIPTIONS: list[ACInfinityPortSelectEntityDescription] = [ ACInfinityPortSelectEntityDescription( - key=PortSettingKey.AT_TYPE, + key=PortControlKey.AT_TYPE, translation_key="active_mode", options=MODE_OPTIONS, + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_active_mode, set_value_fn=__set_value_fn_active_mode, - ) + ), + ACInfinityPortSelectEntityDescription( + key=AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, + translation_key="dynamic_response_type", + options=DYNAMIC_RESPONSE_OPTIONS, + suitable_fn=suitable_fn_port_setting_default, + get_value_fn=__get_value_fn_dynamic_response_type, + set_value_fn=__set_value_fn_dynamic_response_type, + ), ] @@ -88,7 +129,9 @@ def __init__( description: ACInfinityPortSelectEntityDescription, port: ACInfinityPort, ) -> None: - super().__init__(coordinator, port, description.key) + super().__init__( + coordinator, port, description.suitable_fn, description.key, Platform.SELECT + ) self.entity_description = description @property @@ -114,16 +157,11 @@ async def async_setup_entry( controllers = coordinator.ac_infinity.get_all_controller_properties() - entities = [] + entities = ACInfinityEntities() for controller in controllers: for port in controller.ports: for description in PORT_DESCRIPTIONS: entity = ACInfinityPortSelectEntity(coordinator, description, port) - entities.append(entity) - _LOGGER.info( - 'Initializing entity "%s" for platform "%s".', - entity.unique_id, - Platform.SELECT, - ) + entities.append_if_suitable(entity) add_entities_callback(entities) diff --git a/custom_components/ac_infinity/sensor.py b/custom_components/ac_infinity/sensor.py index 799df21..33718f3 100644 --- a/custom_components/ac_infinity/sensor.py +++ b/custom_components/ac_infinity/sensor.py @@ -25,18 +25,22 @@ ACInfinityControllerEntity, ACInfinityControllerReadOnlyMixin, ACInfinityDataUpdateCoordinator, + ACInfinityEntities, ACInfinityEntity, ACInfinityPort, ACInfinityPortEntity, ACInfinityPortReadOnlyMixin, get_value_fn_port_property_default, + suitable_fn_controller_property_default, + suitable_fn_port_control_default, + suitable_fn_port_property_default, ) from .const import ( DOMAIN, ControllerPropertyKey, + PortControlKey, PortPropertyKey, - PortSettingKey, ) _LOGGER = logging.getLogger(__name__) @@ -84,8 +88,8 @@ def __get_value_fn_floating_point_as_int( def __get_value_fn_port_setting_default_zero( entity: ACInfinityEntity, port: ACInfinityPort ): - return entity.ac_infinity.get_port_setting( - port.controller.device_id, port.port_index, PortSettingKey.SURPLUS, 0 + return entity.ac_infinity.get_port_control( + port.controller.device_id, port.port_index, PortControlKey.SURPLUS, 0 ) @@ -98,6 +102,7 @@ def __get_value_fn_port_setting_default_zero( icon=None, # default translation_key="temperature", suggested_unit_of_measurement=None, + suitable_fn=suitable_fn_controller_property_default, get_value_fn=__get_value_fn_floating_point_as_int, ), ACInfinityControllerSensorEntityDescription( @@ -108,6 +113,7 @@ def __get_value_fn_port_setting_default_zero( icon=None, # default translation_key="humidity", suggested_unit_of_measurement=None, + suitable_fn=suitable_fn_controller_property_default, get_value_fn=__get_value_fn_floating_point_as_int, ), ACInfinityControllerSensorEntityDescription( @@ -118,6 +124,7 @@ def __get_value_fn_port_setting_default_zero( native_unit_of_measurement=UnitOfPressure.KPA, icon="mdi:water-thermometer", translation_key="vapor_pressure_deficit", + suitable_fn=suitable_fn_controller_property_default, get_value_fn=__get_value_fn_floating_point_as_int, ), ] @@ -131,16 +138,18 @@ def __get_value_fn_port_setting_default_zero( icon=None, # default translation_key="current_power", suggested_unit_of_measurement=None, + suitable_fn=suitable_fn_port_property_default, get_value_fn=get_value_fn_port_property_default, ), ACInfinityPortSensorEntityDescription( - key=PortSettingKey.SURPLUS, + key=PortControlKey.SURPLUS, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, icon=None, # default translation_key="remaining_time", suggested_unit_of_measurement=None, state_class=None, + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_port_setting_default_zero, ), ] @@ -155,7 +164,13 @@ def __init__( description: ACInfinityControllerSensorEntityDescription, controller: ACInfinityController, ) -> None: - super().__init__(coordinator, controller, description.key) + super().__init__( + coordinator, + controller, + description.suitable_fn, + description.key, + Platform.SENSOR, + ) self.entity_description = description @property @@ -172,7 +187,9 @@ def __init__( description: ACInfinityPortSensorEntityDescription, port: ACInfinityPort, ) -> None: - super().__init__(coordinator, port, description.key) + super().__init__( + coordinator, port, description.suitable_fn, description.key, Platform.SENSOR + ) self.entity_description = description @property @@ -189,27 +206,17 @@ async def async_setup_entry( controllers = coordinator.ac_infinity.get_all_controller_properties() - entities = [] + entities = ACInfinityEntities() for controller in controllers: for description in CONTROLLER_DESCRIPTIONS: entity = ACInfinityControllerSensorEntity( coordinator, description, controller ) - entities.append(entity) - _LOGGER.info( - 'Initializing entity "%s" for platform "%s".', - entity.unique_id, - Platform.SENSOR, - ) + entities.append_if_suitable(entity) for port in controller.ports: for description in PORT_DESCRIPTIONS: entity = ACInfinityPortSensorEntity(coordinator, description, port) - entities.append(entity) - _LOGGER.info( - 'Initializing entity "%s" for platform "%s".', - entity.unique_id, - Platform.SENSOR, - ) + entities.append_if_suitable(entity) add_entities_callback(entities) diff --git a/custom_components/ac_infinity/strings.json b/custom_components/ac_infinity/strings.json index 4935414..1311fe6 100644 --- a/custom_components/ac_infinity/strings.json +++ b/custom_components/ac_infinity/strings.json @@ -96,11 +96,32 @@ }, "vpd_leaf_temperature_offset" : { "name": "VPD Leaf Temperature Offset" + }, + "dynamic_transition_temp": { + "name": "Transition Temperature" + }, + "dynamic_transition_humidity": { + "name": "Transition Humidity" + }, + "dynamic_transition_vpd": { + "name": "Transition VPD" + }, + "dynamic_buffer_temp": { + "name": "Buffer Temperature" + }, + "dynamic_buffer_humidity": { + "name": "Buffer Humidity" + }, + "dynamic_buffer_vpd": { + "name": "Buffer VPD" } }, "select": { "active_mode": { "name": "Active Mode" + }, + "dynamic_response_type": { + "name": "Dynamic Response" } }, "sensor": { diff --git a/custom_components/ac_infinity/switch.py b/custom_components/ac_infinity/switch.py index 1674f3f..ab011fb 100644 --- a/custom_components/ac_infinity/switch.py +++ b/custom_components/ac_infinity/switch.py @@ -15,16 +15,18 @@ SCHEDULE_DISABLED_VALUE, SCHEDULE_EOD_VALUE, SCHEDULE_MIDNIGHT_VALUE, - PortSettingKey, + PortControlKey, ) from custom_components.ac_infinity.core import ( ACInfinityDataUpdateCoordinator, + ACInfinityEntities, ACInfinityEntity, ACInfinityPort, ACInfinityPortEntity, ACInfinityPortReadWriteMixin, - get_value_fn_port_setting_default, - set_value_fn_port_setting_default, + get_value_fn_port_control_default, + set_value_fn_port_control_default, + suitable_fn_port_control_default, ) _LOGGER = logging.getLogger(__name__) @@ -61,7 +63,7 @@ class ACInfinityPortSwitchEntityDescription( def __get_value_fn_schedule_enabled(entity: ACInfinityEntity, port: ACInfinityPort): return ( - entity.ac_infinity.get_port_setting( + entity.ac_infinity.get_port_control( port.controller.device_id, port.port_index, entity.entity_description.key ) < SCHEDULE_EOD_VALUE + 1 @@ -70,84 +72,92 @@ def __get_value_fn_schedule_enabled(entity: ACInfinityEntity, port: ACInfinityPo PORT_DESCRIPTIONS: list[ACInfinityPortSwitchEntityDescription] = [ ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.VPD_HIGH_ENABLED, + key=PortControlKey.VPD_HIGH_ENABLED, device_class=SwitchDeviceClass.SWITCH, on_value=1, off_value=0, icon=None, # default translation_key="vpd_mode_high_enabled", - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.VPD_LOW_ENABLED, + key=PortControlKey.VPD_LOW_ENABLED, device_class=SwitchDeviceClass.SWITCH, on_value=1, off_value=0, icon=None, # default translation_key="vpd_mode_low_enabled", - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.AUTO_TEMP_HIGH_ENABLED, + key=PortControlKey.AUTO_TEMP_HIGH_ENABLED, device_class=SwitchDeviceClass.SWITCH, on_value=1, off_value=0, icon=None, # default translation_key="auto_mode_temp_high_enabled", - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.AUTO_TEMP_LOW_ENABLED, + key=PortControlKey.AUTO_TEMP_LOW_ENABLED, device_class=SwitchDeviceClass.SWITCH, on_value=1, off_value=0, icon=None, # default translation_key="auto_mode_temp_low_enabled", - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.AUTO_HUMIDITY_HIGH_ENABLED, + key=PortControlKey.AUTO_HUMIDITY_HIGH_ENABLED, device_class=SwitchDeviceClass.SWITCH, on_value=1, off_value=0, icon=None, # default translation_key="auto_mode_humidity_high_enabled", - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.AUTO_HUMIDITY_LOW_ENABLED, + key=PortControlKey.AUTO_HUMIDITY_LOW_ENABLED, device_class=SwitchDeviceClass.SWITCH, on_value=1, off_value=0, icon=None, # default translation_key="auto_mode_humidity_low_enabled", - get_value_fn=get_value_fn_port_setting_default, - set_value_fn=set_value_fn_port_setting_default, + suitable_fn=suitable_fn_port_control_default, + get_value_fn=get_value_fn_port_control_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.SCHEDULED_START_TIME, + key=PortControlKey.SCHEDULED_START_TIME, device_class=SwitchDeviceClass.SWITCH, on_value=SCHEDULE_MIDNIGHT_VALUE, off_value=SCHEDULE_DISABLED_VALUE, icon=None, # default translation_key="schedule_mode_on_time_enabled", + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_schedule_enabled, - set_value_fn=set_value_fn_port_setting_default, + set_value_fn=set_value_fn_port_control_default, ), ACInfinityPortSwitchEntityDescription( - key=PortSettingKey.SCHEDULED_END_TIME, + key=PortControlKey.SCHEDULED_END_TIME, device_class=SwitchDeviceClass.SWITCH, on_value=SCHEDULE_EOD_VALUE, off_value=SCHEDULE_DISABLED_VALUE, icon=None, # default translation_key="schedule_mode_off_time_enabled", + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_schedule_enabled, - set_value_fn=set_value_fn_port_setting_default, + set_value_fn=set_value_fn_port_control_default, ), ] @@ -161,7 +171,9 @@ def __init__( description: ACInfinityPortSwitchEntityDescription, port: ACInfinityPort, ) -> None: - super().__init__(coordinator, port, description.key) + super().__init__( + coordinator, port, description.suitable_fn, description.key, Platform.SWITCH + ) self.entity_description = description @property @@ -195,16 +207,11 @@ async def async_setup_entry( controllers = coordinator.ac_infinity.get_all_controller_properties() - entities = [] + entities = ACInfinityEntities() for controller in controllers: for port in controller.ports: for description in PORT_DESCRIPTIONS: entity = ACInfinityPortSwitchEntity(coordinator, description, port) - entities.append(entity) - _LOGGER.info( - 'Initializing entity "%s" for platform "%s".', - entity.unique_id, - Platform.SWITCH, - ) + entities.append_if_suitable(entity) add_entities_callback(entities) diff --git a/custom_components/ac_infinity/time.py b/custom_components/ac_infinity/time.py index ce6a767..3c6ef7b 100644 --- a/custom_components/ac_infinity/time.py +++ b/custom_components/ac_infinity/time.py @@ -5,19 +5,22 @@ from homeassistant.components.time import TimeEntity, TimeEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from custom_components.ac_infinity.const import ( DOMAIN, SCHEDULE_DISABLED_VALUE, - PortSettingKey, + PortControlKey, ) from custom_components.ac_infinity.core import ( ACInfinityDataUpdateCoordinator, + ACInfinityEntities, ACInfinityEntity, ACInfinityPort, ACInfinityPortEntity, ACInfinityPortReadWriteMixin, + suitable_fn_port_control_default, ) _LOGGER = logging.getLogger(__name__) @@ -62,7 +65,7 @@ class ACInfinityPortTimeEntityDescription( def __get_value_fn_time(entity: ACInfinityEntity, port: ACInfinityPort): return __get_time_from_total_minutes( - entity.ac_infinity.get_port_setting( + entity.ac_infinity.get_port_control( port.controller.device_id, port.port_index, entity.entity_description.key, @@ -71,7 +74,7 @@ def __get_value_fn_time(entity: ACInfinityEntity, port: ACInfinityPort): def __set_value_fn_time(entity: ACInfinityEntity, port: ACInfinityPort, value: time): - return entity.ac_infinity.update_port_setting( + return entity.ac_infinity.update_port_control( port.controller.device_id, port.port_index, entity.entity_description.key, @@ -81,16 +84,18 @@ def __set_value_fn_time(entity: ACInfinityEntity, port: ACInfinityPort, value: t PORT_DESCRIPTIONS: list[ACInfinityPortTimeEntityDescription] = [ ACInfinityPortTimeEntityDescription( - key=PortSettingKey.SCHEDULED_START_TIME, + key=PortControlKey.SCHEDULED_START_TIME, icon=None, # default translation_key="schedule_mode_on_time", + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_time, set_value_fn=__set_value_fn_time, ), ACInfinityPortTimeEntityDescription( - key=PortSettingKey.SCHEDULED_END_TIME, + key=PortControlKey.SCHEDULED_END_TIME, icon=None, # default translation_key="schedule_mode_off_time", + suitable_fn=suitable_fn_port_control_default, get_value_fn=__get_value_fn_time, set_value_fn=__set_value_fn_time, ), @@ -106,7 +111,9 @@ def __init__( description: ACInfinityPortTimeEntityDescription, port: ACInfinityPort, ) -> None: - super().__init__(coordinator, port, description.key) + super().__init__( + coordinator, port, description.suitable_fn, description.key, Platform.TIME + ) self.entity_description = description @property @@ -130,11 +137,11 @@ async def async_setup_entry( devices = coordinator.ac_infinity.get_all_controller_properties() - entities = [] + entities = ACInfinityEntities() for device in devices: for port in device.ports: for description in PORT_DESCRIPTIONS: - entities.append( + entities.append_if_suitable( ACInfinityPortTimeEntity(coordinator, description, port) ) diff --git a/custom_components/ac_infinity/translations/en.json b/custom_components/ac_infinity/translations/en.json index f89244d..a367e9c 100644 --- a/custom_components/ac_infinity/translations/en.json +++ b/custom_components/ac_infinity/translations/en.json @@ -97,11 +97,32 @@ }, "vpd_leaf_temperature_offset" : { "name": "VPD Leaf Temperature Offset" + }, + "dynamic_transition_temp": { + "name": "Transition Temperature" + }, + "dynamic_transition_humidity": { + "name": "Transition Humidity" + }, + "dynamic_transition_vpd": { + "name": "Transition VPD" + }, + "dynamic_buffer_temp": { + "name": "Buffer Temperature" + }, + "dynamic_buffer_humidity": { + "name": "Buffer Humidity" + }, + "dynamic_buffer_vpd": { + "name": "Buffer VPD" } }, "select": { "active_mode": { "name": "Active Mode" + }, + "dynamic_response_type": { + "name": "Dynamic Response" } }, "sensor": { diff --git a/tests/__init__.py b/tests/__init__.py index 413560d..a38a38b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -22,12 +22,12 @@ ) from tests.data_models import ( CONTROLLER_PROPERTIES_DATA, - CONTROLLER_SETTINGS_DATA, + DEVICE_SETTINGS_DATA, EMAIL, ENTRY_ID, PASSWORD, + PORT_CONTROLS_DATA, PORT_PROPERTIES_DATA, - PORT_SETTINGS_DATA, ) MockType = Union[ @@ -115,24 +115,30 @@ def setup_entity_mocks(mocker: MockFixture): ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._controller_settings = CONTROLLER_SETTINGS_DATA + ac_infinity._device_settings = DEVICE_SETTINGS_DATA ac_infinity._port_properties = PORT_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA coordinator = ACInfinityDataUpdateCoordinator(hass, ac_infinity, 10) - port_set_mock = mocker.patch.object( - ac_infinity, "update_port_setting", return_value=future + port_control_set_mock = mocker.patch.object( + ac_infinity, "update_port_control", return_value=future ) - port_sets_mock = mocker.patch.object( - ac_infinity, "update_port_settings", return_value=future + port_control_sets_mock = mocker.patch.object( + ac_infinity, "update_port_controls", return_value=future ) - controller_set_mock = mocker.patch.object( + controller_setting_set_mock = mocker.patch.object( ac_infinity, "update_controller_setting", return_value=future ) - controller_sets_mock = mocker.patch.object( + controller_setting_sets_mock = mocker.patch.object( ac_infinity, "update_controller_settings", return_value=future ) + port_setting_set_mock = mocker.patch.object( + ac_infinity, "update_port_setting", return_value=future + ) + port_setting_sets_mock = mocker.patch.object( + ac_infinity, "update_port_settings", return_value=future + ) refresh_mock = mocker.patch.object( coordinator, "async_request_refresh", return_value=future ) @@ -170,10 +176,12 @@ def setup_entity_mocks(mocker: MockFixture): config_entry, entities, ac_infinity, - controller_set_mock, - controller_sets_mock, - port_set_mock, - port_sets_mock, + controller_setting_set_mock, + controller_setting_sets_mock, + port_control_set_mock, + port_control_sets_mock, + port_setting_set_mock, + port_setting_sets_mock, write_ha_mock, coordinator, refresh_mock, @@ -190,8 +198,10 @@ def __init__( ac_infinity, controller_set_mock, controller_sets_mock, - port_set_mock, - port_sets_mock, + port_control_set_mock, + port_control_sets_mock, + port_setting_set_mock, + port_setting_sets_mock, write_ha_mock, coordinator, refresh_mock, @@ -203,8 +213,10 @@ def __init__( self.ac_infinity: ACInfinityService = ac_infinity self.controller_set_mock: MockType = controller_set_mock self.controller_sets_mock: MockType = controller_sets_mock - self.port_set_mock: MockType = port_set_mock - self.port_sets_mock: MockType = port_sets_mock + self.port_control_set_mock: MockType = port_control_set_mock + self.port_control_sets_mock: MockType = port_control_sets_mock + self.port_setting_set_mock: MockType = port_setting_set_mock + self.port_setting_sets_mock: MockType = port_setting_sets_mock self.write_ha_mock: MockType = write_ha_mock self.coordinator: ACInfinityDataUpdateCoordinator = coordinator self.refresh_mock: MockType = refresh_mock diff --git a/tests/data_models.py b/tests/data_models.py index f0651d4..75e1716 100644 --- a/tests/data_models.py +++ b/tests/data_models.py @@ -183,7 +183,7 @@ } # noinspection SpellCheckingInspection -PORT_SETTING = { +PORT_CONTROLS = { "modeSetid": str(MODE_SET_ID), "devId": str(DEVICE_ID), "externalPort": 4, @@ -290,18 +290,18 @@ "devCt": 0, "devCth": 0, "devCh": 0, - "devTth": 0, - "devTt": 0, - "devTh": 0, + "devTth": 8, + "devTt": 4, + "devTh": 5, "devCompany": 0, "vpdCth": 0, "vpdCt": 0, - "vpdTransition": 0, - "devBth": 0, - "devBt": 0, - "devBh": 0, - "devBvpd": 0, - "isFlag": 0, + "vpdTransition": 6, + "devBth": 8, + "devBt": 4, + "devBh": 5, + "devBvpd": 6, + "isFlag": 1, "onTimeSwitch": 0, "onTime": 0, "sensors": None, @@ -326,76 +326,75 @@ "onlyUpdateSpeed": 0, } -# noinspection SpellCheckingInspection -CONTROLLER_SETTINGS = { - "atType": 1, - "backlightSwitch": 1, - "calibrationTime": None, - "devBh": 0, - "devBt": 0, - "devBth": 0, - "devBvpd": 0, - "devCh": 5, - "devCompany": 1, - "devCt": -10, - "devCth": 0, +DEVICE_SETTINGS = { + "setId": str(MODE_SET_ID), "devId": str(DEVICE_ID), - "devLight": 163, "devMacAddr": None, + "port": 1, "devName": None, - "devTh": 0, - "devTimeZone": None, - "devTt": 0, - "devTth": 0, - "ecOrTds": 0, - "ecUnit": 0, "externalPort": 1, + "devLight": 163, "hasBacklightSwitch": 1, + "backlightSwitch": 1, "hasKeytoneSwitch": 0, - "humiCompare": 0, - "interchangeSensor": 0, - "isFlag": 0, - "isOnMinMaxTime": 2, - "isOpenDoseTime": 0, "keytoneSwitch": 1, - "loadType": 0, - "offDoseTime": 0, + "devCt": 0, + "devCh": 5, + "devTt": 2, + "devTh": 3, + "devCompany": 1, + "devTth": 5, + "devCth": 0, + "vpdCth": 0, + "vpdCt": 0, + "vpdTransition": 3, "offSpead": 0, - "onDoseTime": 0, - "onMaxTime": 0, - "onMinTime": 0, + "onSpead": 2, "onSelfSpead": 0, - "onSpead": 1, - "onTime": 0, - "onTimeSwitch": 0, - "otaUpdating": None, - "photocellSwitch": 1, - "port": 1, - "portParamData": "", "portResistance": 3300, - "secFucDevEffect": 0, - "secFucDevtype": 0, - "secFucParamNums": 0, - "secFucParams": "", - "secFucReportTime": 0, - "secFucStatus": 0, - "sensorSetting": None, + "devBth": 2, + "devBt": 1, + "devBh": 2, + "devBvpd": 4, + "isFlag": 1, + "devTimeZone": None, + "loadType": 0, + "tempCompare": 0, + "humiCompare": 0, + "settingMode": 0, + "vpdSettingMode": 0, + "atType": 1, + "onTimeSwitch": 0, + "onTime": 0, "sensorSettingStr": None, - "sensorTransBuff": None, + "sensorSetting": None, "sensorTransBuffStr": None, - "setId": str(MODE_SET_ID), - "settingMode": 0, + "sensorTransBuff": None, + "portParamData": "", + "photocellSwitch": 1, + "isOpenDoseTime": 0, + "onDoseTime": 0, + "offDoseTime": 0, + "isOnMinMaxTime": 2, + "onMinTime": 0, + "onMaxTime": 0, + "ecOrTds": 0, + "ecUnit": 0, + "tdsUnit": 0, + "interchangeSensor": 0, "subDeviceId": None, - "subDeviceType": None, "subDeviceVersion": None, + "subDeviceType": None, "supportOta": None, - "tdsUnit": 0, - "tempCompare": 0, - "updateAllPort": None, - "vpdCt": 10, - "vpdCth": 20, - "vpdSettingMode": 0, - "vpdTransition": 0, + "otaUpdating": None, + "secFucStatus": 0, + "secFucDevtype": 0, + "secFucDevEffect": 0, + "secFucParamNums": 0, + "secFucParams": "", + "secFucReportTime": 0, + "updateAllPort": True, + "calibrationTime": None, } DEVICE_INFO_LIST_ALL = [CONTROLLER_PROPERTIES] @@ -404,21 +403,27 @@ "code": 200, "data": DEVICE_INFO_LIST_ALL, } -GET_DEV_MODE_SETTING_LIST_PAYLOAD = {"msg": "操作成功", "code": 200, "data": PORT_SETTING} -GET_DEV_SETTINGS_PAYLOAD = {"msg": "操作成功", "code": 200, "data": CONTROLLER_SETTINGS} +GET_DEV_MODE_SETTING_LIST_PAYLOAD = {"msg": "操作成功", "code": 200, "data": PORT_CONTROLS} +GET_DEV_SETTINGS_PAYLOAD = {"msg": "操作成功", "code": 200, "data": DEVICE_SETTINGS} UPDATE_SUCCESS_PAYLOAD = {"msg": "操作成功", "code": 200} CONTROLLER_PROPERTIES_DATA = {str(DEVICE_ID): CONTROLLER_PROPERTIES} -CONTROLLER_SETTINGS_DATA = {str(DEVICE_ID): CONTROLLER_SETTINGS} +DEVICE_SETTINGS_DATA = { + (str(DEVICE_ID), 0): DEVICE_SETTINGS, + (str(DEVICE_ID), 1): DEVICE_SETTINGS, + (str(DEVICE_ID), 2): DEVICE_SETTINGS, + (str(DEVICE_ID), 3): DEVICE_SETTINGS, + (str(DEVICE_ID), 4): DEVICE_SETTINGS, +} PORT_PROPERTIES_DATA = { (str(DEVICE_ID), 1): PORT_PROPERTY_ONE, (str(DEVICE_ID), 2): PORT_PROPERTY_TWO, (str(DEVICE_ID), 3): PORT_PROPERTY_THREE, (str(DEVICE_ID), 4): PORT_PROPERTY_FOUR, } -PORT_SETTINGS_DATA = { - (str(DEVICE_ID), 1): PORT_SETTING, - (str(DEVICE_ID), 2): PORT_SETTING, - (str(DEVICE_ID), 3): PORT_SETTING, - (str(DEVICE_ID), 4): PORT_SETTING, +PORT_CONTROLS_DATA = { + (str(DEVICE_ID), 1): PORT_CONTROLS, + (str(DEVICE_ID), 2): PORT_CONTROLS, + (str(DEVICE_ID), 3): PORT_CONTROLS, + (str(DEVICE_ID), 4): PORT_CONTROLS, } diff --git a/tests/test_client.py b/tests/test_client.py index c69c649..4114705 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -13,12 +13,12 @@ ACInfinityClientInvalidAuth, ACInfinityClientRequestFailed, ) -from custom_components.ac_infinity.const import ControllerSettingKey, PortSettingKey +from custom_components.ac_infinity.const import AdvancedSettingsKey, PortControlKey from tests.data_models import ( - CONTROLLER_SETTINGS, DEVICE_ID, DEVICE_INFO_LIST_ALL_PAYLOAD, DEVICE_NAME, + DEVICE_SETTINGS, EMAIL, GET_DEV_MODE_SETTING_LIST_PAYLOAD, GET_DEV_SETTINGS_PAYLOAD, @@ -26,7 +26,7 @@ LOGIN_PAYLOAD, MODE_SET_ID, PASSWORD, - PORT_SETTING, + PORT_CONTROLS, UPDATE_SUCCESS_PAYLOAD, USER_ID, ) @@ -194,7 +194,7 @@ async def __make_generic_set_port_settings_call_and_get_sent_payload( ) await client.set_device_mode_settings( - DEVICE_ID, 4, [(PortSettingKey.ON_SPEED, 2)] + DEVICE_ID, 4, [(PortControlKey.ON_SPEED, 2)] ) gen = (request for request in mocked.requests.values()) @@ -210,23 +210,23 @@ async def test_set_device_port_setting_values_copied_from_get_call(self): await self.__make_generic_set_port_settings_call_and_get_sent_payload() ) - for key in PORT_SETTING: + for key in PORT_CONTROLS: # ignore fields we set or need to modify. They are tested in subsequent test cases. if key not in [ - PortSettingKey.DEV_ID, - PortSettingKey.MODE_SET_ID, - PortSettingKey.ON_SPEED, - PortSettingKey.DEV_ID, - PortSettingKey.MODE_SET_ID, - PortSettingKey.VPD_STATUS, - PortSettingKey.VPD_NUMS, - PortSettingKey.MASTER_PORT, - PortSettingKey.DEV_SETTING, - PortSettingKey.DEVICE_MAC_ADDR, + PortControlKey.DEV_ID, + PortControlKey.MODE_SET_ID, + PortControlKey.ON_SPEED, + PortControlKey.DEV_ID, + PortControlKey.MODE_SET_ID, + PortControlKey.VPD_STATUS, + PortControlKey.VPD_NUMS, + PortControlKey.MASTER_PORT, + PortControlKey.DEV_SETTING, + PortControlKey.DEVICE_MAC_ADDR, ]: assert key in payload, f"Key {key} is missing" assert payload[key] == ( - PORT_SETTING[key] or 0 + PORT_CONTROLS[key] or 0 ), f"Key {key} has incorrect value" async def test_set_device_port_setting_value_changed_in_payload(self): @@ -235,7 +235,7 @@ async def test_set_device_port_setting_value_changed_in_payload(self): await self.__make_generic_set_port_settings_call_and_get_sent_payload() ) - assert payload[PortSettingKey.ON_SPEED] == 2 + assert payload[PortControlKey.ON_SPEED] == 2 @pytest.mark.parametrize("set_value", [0, None, 1]) async def test_set_device_port_setting_zero_even_when_null(self, set_value): @@ -244,24 +244,24 @@ async def test_set_device_port_setting_zero_even_when_null(self, set_value): dev_mode_settings = GET_DEV_MODE_SETTING_LIST_PAYLOAD assert isinstance(dev_mode_settings["data"], dict) - dev_mode_settings["data"][PortSettingKey.SURPLUS] = set_value - dev_mode_settings["data"][PortSettingKey.TARGET_TEMPERATURE_SWITCH] = set_value - dev_mode_settings["data"][PortSettingKey.TARGET_HUMIDITY_SWITCH] = set_value - dev_mode_settings["data"][PortSettingKey.TARGET_VPD_SWITCH] = set_value - dev_mode_settings["data"][PortSettingKey.EC_OR_TDS] = set_value - dev_mode_settings["data"][PortSettingKey.MASTER_PORT] = set_value + dev_mode_settings["data"][PortControlKey.SURPLUS] = set_value + dev_mode_settings["data"][PortControlKey.TARGET_TEMPERATURE_SWITCH] = set_value + dev_mode_settings["data"][PortControlKey.TARGET_HUMIDITY_SWITCH] = set_value + dev_mode_settings["data"][PortControlKey.TARGET_VPD_SWITCH] = set_value + dev_mode_settings["data"][PortControlKey.EC_OR_TDS] = set_value + dev_mode_settings["data"][PortControlKey.MASTER_PORT] = set_value payload = await self.__make_generic_set_port_settings_call_and_get_sent_payload( dev_mode_settings ) expected = set_value if set_value else 0 - assert payload[PortSettingKey.SURPLUS] == expected - assert payload[PortSettingKey.TARGET_HUMIDITY_SWITCH] == expected - assert payload[PortSettingKey.TARGET_TEMPERATURE_SWITCH] == expected - assert payload[PortSettingKey.TARGET_VPD_SWITCH] == expected - assert payload[PortSettingKey.EC_OR_TDS] == expected - assert payload[PortSettingKey.MASTER_PORT] == expected + assert payload[PortControlKey.SURPLUS] == expected + assert payload[PortControlKey.TARGET_HUMIDITY_SWITCH] == expected + assert payload[PortControlKey.TARGET_TEMPERATURE_SWITCH] == expected + assert payload[PortControlKey.TARGET_VPD_SWITCH] == expected + assert payload[PortControlKey.EC_OR_TDS] == expected + assert payload[PortControlKey.MASTER_PORT] == expected async def test_set_device_port_setting_bad_fields_removed_and_missing_fields_added( self, @@ -275,13 +275,13 @@ async def test_set_device_port_setting_bad_fields_removed_and_missing_fields_add ) # bad fields removed - assert PortSettingKey.DEV_SETTING not in payload - assert PortSettingKey.IPC_SETTING not in payload - assert PortSettingKey.DEVICE_MAC_ADDR not in payload + assert PortControlKey.DEV_SETTING not in payload + assert PortControlKey.IPC_SETTING not in payload + assert PortControlKey.DEVICE_MAC_ADDR not in payload # missing fields added - assert PortSettingKey.VPD_STATUS in payload - assert PortSettingKey.VPD_NUMS in payload + assert PortControlKey.VPD_STATUS in payload + assert PortControlKey.VPD_NUMS in payload async def test_set_device_port_setting_dev_id_and_mode_set_id_are_int_values(self): """When setting a value, fields that are not passed to the update call when using the Android/iOS app should @@ -292,17 +292,18 @@ async def test_set_device_port_setting_dev_id_and_mode_set_id_are_int_values(sel await self.__make_generic_set_port_settings_call_and_get_sent_payload() ) - assert PortSettingKey.DEV_ID in payload - dev_id = payload[PortSettingKey.DEV_ID] + assert PortControlKey.DEV_ID in payload + dev_id = payload[PortControlKey.DEV_ID] assert isinstance(dev_id, int) assert dev_id == DEVICE_ID - assert PortSettingKey.MODE_SET_ID in payload - mode_set_id = payload[PortSettingKey.MODE_SET_ID] + assert PortControlKey.MODE_SET_ID in payload + mode_set_id = payload[PortControlKey.MODE_SET_ID] assert isinstance(mode_set_id, int) assert mode_set_id == MODE_SET_ID - async def test_get_controller_settings_returns_settings(self): + @pytest.mark.parametrize("port", [0, 1, 2, 3, 4]) + async def test_get_device_settings_returns_settings(self, port: int): """When logged in, get controller settings should return the current settings""" client = ACInfinityClient(HOST, EMAIL, PASSWORD) client._user_id = USER_ID @@ -314,7 +315,7 @@ async def test_get_controller_settings_returns_settings(self): payload=GET_DEV_SETTINGS_PAYLOAD, ) - result = await client.get_device_settings(DEVICE_ID) + result = await client.get_device_settings(DEVICE_ID, port) assert result is not None assert result["devId"] == f"{DEVICE_ID}" @@ -322,16 +323,16 @@ async def test_get_controller_settings_returns_settings(self): gen = (request for request in mocked.requests.values()) found = next(gen) - assert found[0].kwargs["data"]["port"] == 0 + assert found[0].kwargs["data"]["port"] == port - async def test_get_controller_settings_connect_error_on_not_logged_in(self): + async def test_get_device_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_settings(DEVICE_ID) + await client.get_device_settings(DEVICE_ID, 0) @staticmethod - async def __make_generic_set_controller_settings_call_and_get_sent_payload( + async def __make_generic_update_advanced_settings_call_and_get_sent_payload( dev_settings_payload=GET_DEV_SETTINGS_PAYLOAD, ): client = ACInfinityClient(HOST, EMAIL, PASSWORD) @@ -349,8 +350,8 @@ async def __make_generic_set_controller_settings_call_and_get_sent_payload( payload=UPDATE_SUCCESS_PAYLOAD, ) - await client.update_device_settings( - DEVICE_ID, DEVICE_NAME, [(ControllerSettingKey.CALIBRATE_HUMIDITY, 3)] + await client.update_advanced_settings( + DEVICE_ID, 0, DEVICE_NAME, [(AdvancedSettingsKey.CALIBRATE_HUMIDITY, 3)] ) gen = (request for request in mocked.requests.values()) @@ -358,76 +359,78 @@ async def __make_generic_set_controller_settings_call_and_get_sent_payload( found = next(gen) return found[0].kwargs["data"] - async def test_set_controller_setting_values_copied_from_get_call(self): + async def test_update_advanced_settings_copied_from_get_call(self): """When setting a value, first fetch the existing settings to build the payload""" payload = ( - await self.__make_generic_set_controller_settings_call_and_get_sent_payload() + await self.__make_generic_update_advanced_settings_call_and_get_sent_payload() ) - for key in CONTROLLER_SETTINGS: + for key in DEVICE_SETTINGS: # ignore fields we set or need to modify. They are tested in subsequent test cases. if key not in [ - ControllerSettingKey.SET_ID, - ControllerSettingKey.DEV_MAC_ADDR, - ControllerSettingKey.PORT_RESISTANCE, - ControllerSettingKey.DEV_TIME_ZONE, - ControllerSettingKey.SENSOR_SETTING, - ControllerSettingKey.SENSOR_TRANS_BUFF, - ControllerSettingKey.PORT_PARAM_DATA, - ControllerSettingKey.SUB_DEVICE_VERSION, - ControllerSettingKey.OTA_UPDATING, - ControllerSettingKey.SEC_FUC_REPORT_TIME, - ControllerSettingKey.UPDATE_ALL_PORT, - ControllerSettingKey.CALIBRATION_TIME, - ControllerSettingKey.DEV_ID, - ControllerSettingKey.SUB_DEVICE_TYPE, - ControllerSettingKey.SUPPORT_OTA, - ControllerSettingKey.SUB_DEVICE_ID, - ControllerSettingKey.SENSOR_TRANS_BUFF_STR, - ControllerSettingKey.SENSOR_SETTING_STR, - ControllerSettingKey.PORT_PARAM_DATA, - ControllerSettingKey.DEV_NAME, - ControllerSettingKey.CALIBRATE_HUMIDITY, + AdvancedSettingsKey.SET_ID, + AdvancedSettingsKey.DEV_MAC_ADDR, + AdvancedSettingsKey.PORT_RESISTANCE, + AdvancedSettingsKey.DEV_TIME_ZONE, + AdvancedSettingsKey.SENSOR_SETTING, + AdvancedSettingsKey.SENSOR_TRANS_BUFF, + AdvancedSettingsKey.PORT_PARAM_DATA, + AdvancedSettingsKey.SUB_DEVICE_VERSION, + AdvancedSettingsKey.OTA_UPDATING, + AdvancedSettingsKey.SEC_FUC_REPORT_TIME, + AdvancedSettingsKey.UPDATE_ALL_PORT, + AdvancedSettingsKey.CALIBRATION_TIME, + AdvancedSettingsKey.DEV_ID, + AdvancedSettingsKey.SUB_DEVICE_TYPE, + AdvancedSettingsKey.SUPPORT_OTA, + AdvancedSettingsKey.SUB_DEVICE_ID, + AdvancedSettingsKey.SENSOR_TRANS_BUFF_STR, + AdvancedSettingsKey.SENSOR_SETTING_STR, + AdvancedSettingsKey.PORT_PARAM_DATA, + AdvancedSettingsKey.DEV_NAME, + AdvancedSettingsKey.CALIBRATE_HUMIDITY, ]: assert key in payload, f"Key {key} is missing" assert ( - payload[key] == CONTROLLER_SETTINGS[key] or 0 + payload[key] == DEVICE_SETTINGS[key] or 0 ), f"Key {key} has incorrect value" - async def test_set_controller_setting_value_changed_in_payload(self): + async def test_update_advanced_settings_value_changed_in_payload(self): """When setting a value, the value is updated in the built payload before sending""" payload = ( - await self.__make_generic_set_controller_settings_call_and_get_sent_payload() + await self.__make_generic_update_advanced_settings_call_and_get_sent_payload() ) - assert payload[ControllerSettingKey.CALIBRATE_HUMIDITY] == 3 + assert payload[AdvancedSettingsKey.CALIBRATE_HUMIDITY] == 3 - async def test_set_device_setting_bad_fields_removed_and_missing_fields_added(self): + async def test_update_advanced_settings_bad_fields_removed_and_missing_fields_added( + self, + ): payload = ( - await self.__make_generic_set_controller_settings_call_and_get_sent_payload() + await self.__make_generic_update_advanced_settings_call_and_get_sent_payload() ) # bad fields stripped before sending - assert ControllerSettingKey.SET_ID not in payload - assert ControllerSettingKey.DEV_MAC_ADDR not in payload - assert ControllerSettingKey.PORT_RESISTANCE not in payload - assert ControllerSettingKey.DEV_TIME_ZONE not in payload - assert ControllerSettingKey.SENSOR_SETTING not in payload - assert ControllerSettingKey.SENSOR_TRANS_BUFF not in payload - assert ControllerSettingKey.SUB_DEVICE_VERSION not in payload - assert ControllerSettingKey.SEC_FUC_REPORT_TIME not in payload - assert ControllerSettingKey.UPDATE_ALL_PORT not in payload - assert ControllerSettingKey.CALIBRATION_TIME not in payload + assert AdvancedSettingsKey.SET_ID not in payload + assert AdvancedSettingsKey.DEV_MAC_ADDR not in payload + assert AdvancedSettingsKey.PORT_RESISTANCE not in payload + assert AdvancedSettingsKey.DEV_TIME_ZONE not in payload + assert AdvancedSettingsKey.SENSOR_SETTING not in payload + assert AdvancedSettingsKey.SENSOR_TRANS_BUFF not in payload + assert AdvancedSettingsKey.SUB_DEVICE_VERSION not in payload + assert AdvancedSettingsKey.SEC_FUC_REPORT_TIME not in payload + assert AdvancedSettingsKey.UPDATE_ALL_PORT not in payload + assert AdvancedSettingsKey.CALIBRATION_TIME not in payload # missing fields added before seending - assert ControllerSettingKey.SENSOR_ONE_TYPE in payload - assert ControllerSettingKey.IS_SHARE in payload - assert ControllerSettingKey.TARGET_VPD_SWITCH in payload - assert ControllerSettingKey.SENSOR_TWO_TYPE in payload - assert ControllerSettingKey.PARAM_SENSORS in payload - assert ControllerSettingKey.ZONE_SENSOR_TYPE in payload + assert AdvancedSettingsKey.SENSOR_ONE_TYPE in payload + assert AdvancedSettingsKey.IS_SHARE in payload + assert AdvancedSettingsKey.TARGET_VPD_SWITCH in payload + assert AdvancedSettingsKey.SENSOR_TWO_TYPE in payload + assert AdvancedSettingsKey.PARAM_SENSORS in payload + assert AdvancedSettingsKey.ZONE_SENSOR_TYPE in payload @pytest.mark.parametrize("set_value", [0, None, 1]) async def test_set_device_setting_zero_even_when_null( @@ -438,22 +441,22 @@ async def test_set_device_setting_zero_even_when_null( dev_settings = GET_DEV_SETTINGS_PAYLOAD assert isinstance(dev_settings["data"], dict) - dev_settings["data"][ControllerSettingKey.OTA_UPDATING] = set_value - dev_settings["data"][ControllerSettingKey.SUB_DEVICE_ID] = set_value - dev_settings["data"][ControllerSettingKey.SUB_DEVICE_TYPE] = set_value - dev_settings["data"][ControllerSettingKey.SUPPORT_OTA] = set_value + dev_settings["data"][AdvancedSettingsKey.OTA_UPDATING] = set_value + dev_settings["data"][AdvancedSettingsKey.SUB_DEVICE_ID] = set_value + dev_settings["data"][AdvancedSettingsKey.SUB_DEVICE_TYPE] = set_value + dev_settings["data"][AdvancedSettingsKey.SUPPORT_OTA] = set_value """When fetching existing settings before update, specified fields should be set to 0 if existing is null""" payload = ( - await self.__make_generic_set_controller_settings_call_and_get_sent_payload() + await self.__make_generic_update_advanced_settings_call_and_get_sent_payload() ) # certain None fields defaulted to 0 before sending. expected = set_value if set_value else 0 - assert payload[ControllerSettingKey.OTA_UPDATING] == expected - assert payload[ControllerSettingKey.SUB_DEVICE_ID] == expected - assert payload[ControllerSettingKey.SUB_DEVICE_TYPE] == expected - assert payload[ControllerSettingKey.SUPPORT_OTA] == expected + assert payload[AdvancedSettingsKey.OTA_UPDATING] == expected + assert payload[AdvancedSettingsKey.SUB_DEVICE_ID] == expected + assert payload[AdvancedSettingsKey.SUB_DEVICE_TYPE] == expected + assert payload[AdvancedSettingsKey.SUPPORT_OTA] == expected async def test_set_device_setting_dev_id_and_mode_set_id_are_int_values(self): """When setting a value, fields that are not passed to the update call when using the Android/iOS app should @@ -461,11 +464,11 @@ async def test_set_device_setting_dev_id_and_mode_set_id_are_int_values(self): as to not change controller settings unnecessarily.""" payload = ( - await self.__make_generic_set_controller_settings_call_and_get_sent_payload() + await self.__make_generic_update_advanced_settings_call_and_get_sent_payload() ) - assert PortSettingKey.DEV_ID in payload - dev_id = payload[ControllerSettingKey.DEV_ID] + assert PortControlKey.DEV_ID in payload + dev_id = payload[AdvancedSettingsKey.DEV_ID] assert isinstance(dev_id, int) assert dev_id == DEVICE_ID @@ -477,24 +480,22 @@ async def test_set_device_settings_null_str_fields_set_to_empty_string( dev_settings = GET_DEV_SETTINGS_PAYLOAD assert isinstance(dev_settings["data"], dict) - dev_settings["data"][ControllerSettingKey.PARAM_SENSORS] = set_value + dev_settings["data"][AdvancedSettingsKey.PARAM_SENSORS] = set_value - payload = ( - await self.__make_generic_set_controller_settings_call_and_get_sent_payload( - dev_settings - ) + payload = await self.__make_generic_update_advanced_settings_call_and_get_sent_payload( + dev_settings ) expected = set_value if set_value else "" - assert payload[ControllerSettingKey.PARAM_SENSORS] == expected - assert payload[ControllerSettingKey.SENSOR_TRANS_BUFF_STR] == "" - assert payload[ControllerSettingKey.SENSOR_SETTING_STR] == "" - assert payload[ControllerSettingKey.PORT_PARAM_DATA] == "" + assert payload[AdvancedSettingsKey.PARAM_SENSORS] == expected + assert payload[AdvancedSettingsKey.SENSOR_TRANS_BUFF_STR] == "" + assert payload[AdvancedSettingsKey.SENSOR_SETTING_STR] == "" + assert payload[AdvancedSettingsKey.PORT_PARAM_DATA] == "" async def test_set_device_settings_dev_name_pulled_from_existing_value(self): payload = ( - await self.__make_generic_set_controller_settings_call_and_get_sent_payload() + await self.__make_generic_update_advanced_settings_call_and_get_sent_payload() ) - assert ControllerSettingKey.DEV_NAME in payload - assert payload[ControllerSettingKey.DEV_NAME] == DEVICE_NAME + assert AdvancedSettingsKey.DEV_NAME in payload + assert payload[AdvancedSettingsKey.DEV_NAME] == DEVICE_NAME diff --git a/tests/test_core.py b/tests/test_core.py index 70f10fd..ed4ee14 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,6 +2,8 @@ from asyncio import Future import pytest +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import UnitOfTemperature from pytest_mock import MockFixture from pytest_mock.plugin import MockType @@ -9,28 +11,44 @@ from custom_components.ac_infinity.const import ( DOMAIN, MANUFACTURER, + AdvancedSettingsKey, ControllerPropertyKey, - ControllerSettingKey, + PortControlKey, PortPropertyKey, - PortSettingKey, ) -from custom_components.ac_infinity.core import ACInfinityService +from custom_components.ac_infinity.core import ( + ACInfinityController, + ACInfinityEntities, + ACInfinityService, +) +from custom_components.ac_infinity.sensor import ( + ACInfinityControllerSensorEntity, + ACInfinityControllerSensorEntityDescription, +) +from . import ACTestObjects, setup_entity_mocks from .data_models import ( + CONTROLLER_PROPERTIES, CONTROLLER_PROPERTIES_DATA, - CONTROLLER_SETTINGS_DATA, DEVICE_ID, DEVICE_INFO_LIST_ALL, DEVICE_NAME, + DEVICE_SETTINGS_DATA, EMAIL, GET_DEV_MODE_SETTING_LIST_PAYLOAD, GET_DEV_SETTINGS_PAYLOAD, MAC_ADDR, PASSWORD, - PORT_SETTINGS_DATA, + PORT_CONTROLS_DATA, + PORT_PROPERTIES_DATA, ) +@pytest.fixture +def setup(mocker: MockFixture): + return setup_entity_mocks(mocker) + + @pytest.mark.asyncio class TestACInfinity: async def test_update_logged_in_should_be_called_if_not_logged_in( @@ -146,6 +164,27 @@ async def test_update_retried_on_failure(self, mocker: MockFixture): assert mock_get_all.call_count == 3 + @pytest.mark.parametrize( + "property_key, value", + [ + (ControllerPropertyKey.DEVICE_NAME, True), + (ControllerPropertyKey.MAC_ADDR, True), + (ControllerPropertyKey.TEMPERATURE, True), + (ControllerPropertyKey.HUMIDITY, True), + ("keyNoExist", False), + ], + ) + @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID), "12345"]) + async def test_get_controller_property_exists_returns_correct_value( + self, device_id, property_key: str, value + ): + """getting a device property returns the correct value""" + ac_infinity = ACInfinityService(EMAIL, PASSWORD) + ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + + result = ac_infinity.get_controller_property_exists(device_id, property_key) + assert result == (value if device_id != "12345" else False) + @pytest.mark.parametrize( "property_key, value", [ @@ -184,6 +223,26 @@ async def test_get_controller_property_returns_null_properly( result = ac_infinity.get_controller_property(device_id, property_key) assert result is None + @pytest.mark.parametrize( + "property_key, value", + [ + (PortPropertyKey.SPEAK, True), + (PortPropertyKey.NAME, True), + ("keyNoExist", False), + ], + ) + @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID), "12345"]) + async def test_get_port_property_exists_returns_correct_value( + self, device_id, property_key: str, value + ): + """getting a port property gets the correct property from the correct port""" + ac_infinity = ACInfinityService(EMAIL, PASSWORD) + ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._port_properties = PORT_PROPERTIES_DATA + + result = ac_infinity.get_port_property_exists(device_id, 1, property_key) + assert result == (value if device_id != "12345" else False) + @pytest.mark.parametrize( "property_key, port_num, value", [ @@ -200,6 +259,7 @@ async def test_get_port_property_gets_correct_property( """getting a port property gets the correct property from the correct port""" ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._port_properties = PORT_PROPERTIES_DATA result = ac_infinity.get_port_property(device_id, port_num, property_key) assert result == value @@ -220,6 +280,7 @@ async def test_get_port_property_returns_null_properly( """the absence of a value should return None instead of keyerror""" ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._port_properties = PORT_PROPERTIES_DATA result = ac_infinity.get_port_property(device_id, port_num, property_key) assert result is None @@ -249,7 +310,11 @@ async def test_get_device_all_device_meta_data_returns_empty_list(self, data): @pytest.mark.parametrize( "dev_type,expected_model", - [(11, "UIS Controller 69 Pro (CTR69P)"), (3, "UIS Controller Type 3")], + [ + (11, "UIS Controller 69 Pro (CTR69P)"), + (18, "UIS CONTROLLER 69 Pro+ (CTR69Q)"), + (3, "UIS Controller Type 3"), + ], ) async def test_ac_infinity_device_has_correct_device_info( self, dev_type: int, expected_model: str @@ -274,48 +339,93 @@ async def test_ac_infinity_device_has_correct_device_info( @pytest.mark.parametrize( "setting_key, value", [ - (PortSettingKey.ON_SPEED, 5), + (PortControlKey.ON_SPEED, True), + (PortControlKey.AT_TYPE, True), + (AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, True), + (AdvancedSettingsKey.DYNAMIC_BUFFER_VPD, True), + ("keyNoExist", False), + ], + ) + @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID), "12345"]) + async def test_get_port_control_exists_returns_correct_value( + self, device_id, setting_key, value + ): + """getting a port setting gets the correct setting from the correct port""" + ac_infinity = ACInfinityService(EMAIL, PASSWORD) + ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA + + result = ac_infinity.get_port_control_exists(device_id, 1, setting_key) + assert result == (value if device_id != "12345" else False) + + @pytest.mark.parametrize( + "setting_key, value", + [ + (PortControlKey.ON_SPEED, 5), ( - PortSettingKey.OFF_SPEED, + PortControlKey.OFF_SPEED, 0, ), # make sure 0 still returns 0 and not None or default - (PortSettingKey.AT_TYPE, 2), + (PortControlKey.AT_TYPE, 2), + (AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, 1), + (AdvancedSettingsKey.DYNAMIC_BUFFER_VPD, 6), ], ) @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID)]) - async def test_get_port_setting_gets_correct_property( + async def test_get_port_control_gets_correct_setting( self, device_id, setting_key, value ): """getting a port setting gets the correct setting from the correct port""" ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA - result = ac_infinity.get_port_setting(device_id, 1, setting_key) + result = ac_infinity.get_port_control(device_id, 1, setting_key) assert result == value @pytest.mark.parametrize("default_value", [0, None, 5455]) @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID)]) - async def test_get_port_setting_gets_returns_default_if_value_is_null( + async def test_get_port_control_gets_returns_default_if_value_is_null( self, device_id, default_value ): """getting a port setting returns 0 instead of null if the key exists but the value is null""" ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA - ac_infinity._port_settings[(str(DEVICE_ID), 1)][PortSettingKey.SURPLUS] = None + ac_infinity._port_controls[(str(DEVICE_ID), 1)][PortControlKey.SURPLUS] = None - result = ac_infinity.get_port_setting( - device_id, 1, PortSettingKey.SURPLUS, default_value=default_value + result = ac_infinity.get_port_control( + device_id, 1, PortControlKey.SURPLUS, default_value=default_value ) assert result == default_value @pytest.mark.parametrize( "setting_key, value", [ - (ControllerSettingKey.CALIBRATE_HUMIDITY, 5), - (ControllerSettingKey.TEMP_UNIT, 1), + (AdvancedSettingsKey.CALIBRATE_HUMIDITY, True), + (AdvancedSettingsKey.TEMP_UNIT, True), + ("keyNoExist", False), + ], + ) + @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID), "12345"]) + async def test_get_controller_setting_exists_returns_correct_value( + self, device_id, setting_key, value + ): + """getting a port setting gets the correct setting from the correct port""" + ac_infinity = ACInfinityService(EMAIL, PASSWORD) + ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._device_settings = DEVICE_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA + + result = ac_infinity.get_controller_setting_exists(device_id, setting_key) + assert result == (value if device_id != "12345" else False) + + @pytest.mark.parametrize( + "setting_key, value", + [ + (AdvancedSettingsKey.CALIBRATE_HUMIDITY, 5), + (AdvancedSettingsKey.TEMP_UNIT, 1), ], ) @pytest.mark.parametrize("device_id", [DEVICE_ID, str(DEVICE_ID)]) @@ -325,8 +435,8 @@ async def test_get_controller_setting_gets_correct_property( """getting a port setting gets the correct setting from the correct port""" ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._controller_settings = CONTROLLER_SETTINGS_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._device_settings = DEVICE_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA result = ac_infinity.get_controller_setting(device_id, setting_key) assert result == value @@ -339,15 +449,15 @@ async def test_get_controller_setting_gets_returns_default_if_value_is_null( """getting a port setting returns 0 instead of null if the key exists but the value is null""" ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA - ac_infinity._port_settings[(str(DEVICE_ID), 1)][ - ControllerSettingKey.CALIBRATE_HUMIDITY + ac_infinity._port_controls[(str(DEVICE_ID), 1)][ + AdvancedSettingsKey.CALIBRATE_HUMIDITY ] = None result = ac_infinity.get_controller_setting( device_id, - ControllerSettingKey.CALIBRATE_HUMIDITY, + AdvancedSettingsKey.CALIBRATE_HUMIDITY, default_value=default_value, ) assert result == default_value @@ -355,14 +465,14 @@ async def test_get_controller_setting_gets_returns_default_if_value_is_null( @pytest.mark.parametrize( "setting_key, device_id", [ - (PortSettingKey.ON_SPEED, "232161"), + (PortControlKey.ON_SPEED, "232161"), ("MyFakeField", DEVICE_ID), (PortPropertyKey.NAME, DEVICE_ID), ("MyFakeField", str(DEVICE_ID)), (PortPropertyKey.NAME, str(DEVICE_ID)), ], ) - async def test_get_port_setting_returns_null_properly( + async def test_get_port_control_returns_null_properly( self, setting_key, device_id, @@ -370,12 +480,12 @@ async def test_get_port_setting_returns_null_properly( """the absence of a value should return None instead of keyerror""" ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA - result = ac_infinity.get_port_setting(device_id, 1, setting_key) + result = ac_infinity.get_port_control(device_id, 1, setting_key) assert result is None - async def test_update_port_setting(self, mocker: MockFixture): + async def test_update_port_control(self, mocker: MockFixture): future: Future = asyncio.Future() future.set_result(None) @@ -386,13 +496,13 @@ async def test_update_port_setting(self, mocker: MockFixture): ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA - await ac_infinity.update_port_setting(DEVICE_ID, 1, PortSettingKey.AT_TYPE, 2) + await ac_infinity.update_port_control(DEVICE_ID, 1, PortControlKey.AT_TYPE, 2) - mocked_set.assert_called_with(DEVICE_ID, 1, [(PortSettingKey.AT_TYPE, 2)]) + mocked_set.assert_called_with(DEVICE_ID, 1, [(PortControlKey.AT_TYPE, 2)]) - async def test_update_port_settings(self, mocker: MockFixture): + async def test_update_port_controls(self, mocker: MockFixture): future: Future = asyncio.Future() future.set_result(None) @@ -403,15 +513,15 @@ async def test_update_port_settings(self, mocker: MockFixture): ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA - await ac_infinity.update_port_settings( - DEVICE_ID, 1, [(PortSettingKey.AT_TYPE, 2)] + await ac_infinity.update_port_controls( + DEVICE_ID, 1, [(PortControlKey.AT_TYPE, 2)] ) - mocked_sets.assert_called_with(DEVICE_ID, 1, [(PortSettingKey.AT_TYPE, 2)]) + mocked_sets.assert_called_with(DEVICE_ID, 1, [(PortControlKey.AT_TYPE, 2)]) - async def test_update_port_settings_retried_on_failure(self, mocker: MockFixture): + async def test_update_port_controls_retried_on_failure(self, mocker: MockFixture): """updating settings should be tried 3 times before failing""" future: Future = asyncio.Future() future.set_result(None) @@ -426,11 +536,11 @@ async def test_update_port_settings_retried_on_failure(self, mocker: MockFixture ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA with pytest.raises(Exception): - await ac_infinity.update_port_settings( - DEVICE_ID, 1, [(PortSettingKey.AT_TYPE, 2)] + await ac_infinity.update_port_controls( + DEVICE_ID, 1, [(PortControlKey.AT_TYPE, 2)] ) assert mocked_sets.call_count == 3 @@ -441,18 +551,18 @@ async def test_update_controller_setting(self, mocker: MockFixture): mocker.patch.object(ACInfinityClient, "is_logged_in", return_value=True) mocked_set = mocker.patch.object( - ACInfinityClient, "update_device_settings", return_value=future + ACInfinityClient, "update_advanced_settings", return_value=future ) ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA await ac_infinity.update_controller_setting( - DEVICE_ID, ControllerSettingKey.CALIBRATE_HUMIDITY, 2 + DEVICE_ID, AdvancedSettingsKey.CALIBRATE_HUMIDITY, 2 ) mocked_set.assert_called_with( - DEVICE_ID, DEVICE_NAME, [(ControllerSettingKey.CALIBRATE_HUMIDITY, 2)] + DEVICE_ID, 0, DEVICE_NAME, [(AdvancedSettingsKey.CALIBRATE_HUMIDITY, 2)] ) async def test_update_controller_settings(self, mocker: MockFixture): @@ -461,19 +571,19 @@ async def test_update_controller_settings(self, mocker: MockFixture): mocker.patch.object(ACInfinityClient, "is_logged_in", return_value=True) mocked_set = mocker.patch.object( - ACInfinityClient, "update_device_settings", return_value=future + ACInfinityClient, "update_advanced_settings", return_value=future ) ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._controller_settings = CONTROLLER_SETTINGS_DATA + ac_infinity._device_settings = DEVICE_SETTINGS_DATA await ac_infinity.update_controller_settings( - DEVICE_ID, [(ControllerSettingKey.CALIBRATE_HUMIDITY, 2)] + DEVICE_ID, [(AdvancedSettingsKey.CALIBRATE_HUMIDITY, 2)] ) mocked_set.assert_called_with( - DEVICE_ID, DEVICE_NAME, [(ControllerSettingKey.CALIBRATE_HUMIDITY, 2)] + DEVICE_ID, 0, DEVICE_NAME, [(AdvancedSettingsKey.CALIBRATE_HUMIDITY, 2)] ) async def test_update_controller_settings_retried_on_failure( @@ -486,18 +596,123 @@ async def test_update_controller_settings_retried_on_failure( mocker.patch.object(ACInfinityClient, "is_logged_in", return_value=True) mocked_sets = mocker.patch.object( ACInfinityClient, - "update_device_settings", + "update_advanced_settings", return_value=future, side_effect=Exception("unit-test"), ) ac_infinity = ACInfinityService(EMAIL, PASSWORD) ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA - ac_infinity._port_settings = PORT_SETTINGS_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA with pytest.raises(Exception): await ac_infinity.update_controller_settings( - DEVICE_ID, [(ControllerSettingKey.CALIBRATE_HUMIDITY, 2)] + DEVICE_ID, [(AdvancedSettingsKey.CALIBRATE_HUMIDITY, 2)] ) assert mocked_sets.call_count == 3 + + async def test_update_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, "update_advanced_settings", return_value=future + ) + + ac_infinity = ACInfinityService(EMAIL, PASSWORD) + ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._port_properties = PORT_PROPERTIES_DATA + ac_infinity._port_properties[(str(DEVICE_ID), 1)][ + PortPropertyKey.NAME + ] = DEVICE_NAME + + await ac_infinity.update_port_setting( + DEVICE_ID, 1, AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, 2 + ) + + mocked_set.assert_called_with( + DEVICE_ID, + 1, + DEVICE_NAME, + [(AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, 2)], + ) + + async def test_update_port_settings(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, "update_advanced_settings", return_value=future + ) + + ac_infinity = ACInfinityService(EMAIL, PASSWORD) + ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._port_properties = PORT_PROPERTIES_DATA + ac_infinity._port_properties[(str(DEVICE_ID), 1)][ + PortPropertyKey.NAME + ] = DEVICE_NAME + + await ac_infinity.update_port_settings( + DEVICE_ID, 1, [(AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, 2)] + ) + + mocked_set.assert_called_with( + DEVICE_ID, + 1, + DEVICE_NAME, + [(AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, 2)], + ) + + async def test_update_port_settings_retried_on_failure(self, mocker: MockFixture): + """updating settings should be tried 3 times before failing""" + future: Future = asyncio.Future() + future.set_result(None) + mocker.patch("asyncio.sleep", return_value=future) + mocker.patch.object(ACInfinityClient, "is_logged_in", return_value=True) + mocked_sets = mocker.patch.object( + ACInfinityClient, + "update_advanced_settings", + return_value=future, + side_effect=Exception("unit-test"), + ) + + ac_infinity = ACInfinityService(EMAIL, PASSWORD) + ac_infinity._controller_properties = CONTROLLER_PROPERTIES_DATA + ac_infinity._port_controls = PORT_CONTROLS_DATA + + with pytest.raises(Exception): + await ac_infinity.update_port_settings( + DEVICE_ID, 1, [(AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, 2)] + ) + + assert mocked_sets.call_count == 3 + + @pytest.mark.parametrize("is_suitable", [True, False]) + async def test_append_if_suitable_only_added_if_suitable(self, setup, is_suitable): + test_objects: ACTestObjects = setup + + description = ACInfinityControllerSensorEntityDescription( + key=ControllerPropertyKey.TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + icon=None, # default + translation_key="temperature", + suggested_unit_of_measurement=None, + suitable_fn=lambda e, c: is_suitable, + get_value_fn=lambda e, c: None, + ) + + entity = ACInfinityControllerSensorEntity( + test_objects.coordinator, + description, + ACInfinityController(CONTROLLER_PROPERTIES), + ) + + entities = ACInfinityEntities() + entities.append_if_suitable(entity) + + assert len(entities) == (1 if is_suitable else 0) diff --git a/tests/test_number.py b/tests/test_number.py index f4ca7d5..4aa407b 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -7,11 +7,12 @@ from custom_components.ac_infinity.const import ( DOMAIN, - ControllerSettingKey, - PortSettingKey, + AdvancedSettingsKey, + PortControlKey, ) from custom_components.ac_infinity.number import ( CONTROLLER_DESCRIPTIONS, + PORT_DESCRIPTIONS, ACInfinityControllerNumberEntity, ACInfinityPortNumberEntity, async_setup_entry, @@ -42,10 +43,10 @@ async def test_async_setup_all_sensors_created(self, setup): test_objects.entities.add_entities_callback, ) - assert len(test_objects.entities._added_entities) == 51 + assert len(test_objects.entities._added_entities) == 75 @pytest.mark.parametrize( - "setting", [PortSettingKey.OFF_SPEED, PortSettingKey.ON_SPEED] + "setting", [PortControlKey.OFF_SPEED, PortControlKey.ON_SPEED] ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_setup_entry_current_speed_created_for_each_port( @@ -67,7 +68,7 @@ async def test_async_setup_entry_current_speed_created_for_each_port( @pytest.mark.parametrize( "setting,expected", - [(PortSettingKey.OFF_SPEED, 0), (PortSettingKey.ON_SPEED, 5)], + [(PortControlKey.OFF_SPEED, 0), (PortControlKey.ON_SPEED, 5)], ) async def test_async_update_current_speed_value_correct( self, setup, setting, expected @@ -83,7 +84,7 @@ async def test_async_update_current_speed_value_correct( test_objects.write_ha_mock.assert_called() @pytest.mark.parametrize( - "setting", [PortSettingKey.OFF_SPEED, PortSettingKey.ON_SPEED] + "setting", [PortControlKey.OFF_SPEED, PortControlKey.ON_SPEED] ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_set_native_value(self, setup, setting, port): @@ -99,12 +100,14 @@ async def test_async_set_native_value(self, setup, setting, port): assert isinstance(entity, ACInfinityPortNumberEntity) await entity.async_set_native_value(4) - test_objects.port_set_mock.assert_called_with(str(DEVICE_ID), port, setting, 4) + test_objects.port_control_set_mock.assert_called_with( + str(DEVICE_ID), port, setting, 4 + ) test_objects.refresh_mock.assert_called() @pytest.mark.parametrize( "key", - [PortSettingKey.TIMER_DURATION_TO_ON, PortSettingKey.TIMER_DURATION_TO_OFF], + [PortControlKey.TIMER_DURATION_TO_ON, PortControlKey.TIMER_DURATION_TO_OFF], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_setup_timer_created_for_each_port(self, setup, key, port): @@ -117,7 +120,7 @@ async def test_async_setup_timer_created_for_each_port(self, setup, key, port): @pytest.mark.parametrize( "setting", - [PortSettingKey.TIMER_DURATION_TO_ON, PortSettingKey.TIMER_DURATION_TO_OFF], + [PortControlKey.TIMER_DURATION_TO_ON, PortControlKey.TIMER_DURATION_TO_OFF], ) @pytest.mark.parametrize( "value,expected", @@ -139,7 +142,7 @@ async def test_async_update_timer_value_correct( setup, async_setup_entry, port, setting ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][setting] = value + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][setting] = value entity._handle_coordinator_update() assert isinstance(entity, ACInfinityPortNumberEntity) @@ -152,7 +155,7 @@ async def test_async_update_timer_value_correct( ) @pytest.mark.parametrize( "setting", - [PortSettingKey.TIMER_DURATION_TO_ON, PortSettingKey.TIMER_DURATION_TO_OFF], + [PortControlKey.TIMER_DURATION_TO_ON, PortControlKey.TIMER_DURATION_TO_OFF], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_set_native_value_timer( @@ -171,7 +174,7 @@ async def test_async_set_native_value_timer( assert isinstance(entity, ACInfinityPortNumberEntity) await entity.async_set_native_value(field_value) - test_objects.port_set_mock.assert_called_with( + test_objects.port_control_set_mock.assert_called_with( str(DEVICE_ID), port, setting, expected ) test_objects.refresh_mock.assert_called() @@ -179,8 +182,8 @@ async def test_async_set_native_value_timer( @pytest.mark.parametrize( "key,label", [ - (PortSettingKey.VPD_HIGH_TRIGGER, "High"), - (PortSettingKey.VPD_LOW_TRIGGER, "Low"), + (PortControlKey.VPD_HIGH_TRIGGER, "High"), + (PortControlKey.VPD_LOW_TRIGGER, "Low"), ], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) @@ -197,8 +200,8 @@ async def test_async_setup_vpd_trigger_setup_for_each_port( @pytest.mark.parametrize( "setting,enabled_setting", [ - (PortSettingKey.VPD_LOW_TRIGGER, PortSettingKey.VPD_LOW_ENABLED), - (PortSettingKey.VPD_HIGH_TRIGGER, PortSettingKey.VPD_HIGH_ENABLED), + (PortControlKey.VPD_LOW_TRIGGER, PortControlKey.VPD_LOW_ENABLED), + (PortControlKey.VPD_HIGH_TRIGGER, PortControlKey.VPD_HIGH_ENABLED), ], ) @pytest.mark.parametrize( @@ -216,7 +219,7 @@ async def test_async_update_value_vpd( setup, async_setup_entry, port, setting ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][setting] = value + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][setting] = value entity._handle_coordinator_update() assert isinstance(entity, ACInfinityPortNumberEntity) @@ -226,8 +229,8 @@ async def test_async_update_value_vpd( @pytest.mark.parametrize( "setting", [ - PortSettingKey.VPD_LOW_TRIGGER, - PortSettingKey.VPD_HIGH_TRIGGER, + PortControlKey.VPD_LOW_TRIGGER, + PortControlKey.VPD_HIGH_TRIGGER, ], ) @pytest.mark.parametrize( @@ -244,7 +247,7 @@ async def test_async_set_native_value_vpd( test_objects: ACTestObjects = setup - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][ + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][ setting ] = prev_value entity = await execute_and_get_port_entity( @@ -254,14 +257,13 @@ async def test_async_set_native_value_vpd( assert isinstance(entity, ACInfinityPortNumberEntity) await entity.async_set_native_value(value) - test_objects.port_set_mock.assert_called_with( + test_objects.port_control_set_mock.assert_called_with( str(DEVICE_ID), port, setting, expected ) test_objects.refresh_mock.assert_called() - # @pytest.mark.parametrize( - "key", [PortSettingKey.CYCLE_DURATION_ON, PortSettingKey.CYCLE_DURATION_OFF] + "key", [PortControlKey.CYCLE_DURATION_ON, PortControlKey.CYCLE_DURATION_OFF] ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_setup_cycle_timer_created_for_each_port( @@ -275,7 +277,7 @@ async def test_async_setup_cycle_timer_created_for_each_port( assert entity.device_info is not None @pytest.mark.parametrize( - "setting", [PortSettingKey.CYCLE_DURATION_ON, PortSettingKey.CYCLE_DURATION_OFF] + "setting", [PortControlKey.CYCLE_DURATION_ON, PortControlKey.CYCLE_DURATION_OFF] ) @pytest.mark.parametrize( "value,expected", @@ -297,7 +299,7 @@ async def test_async_update_cycle_timer_value_correct( setup, async_setup_entry, port, setting ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][setting] = value + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][setting] = value entity._handle_coordinator_update() assert isinstance(entity, ACInfinityPortNumberEntity) @@ -309,7 +311,7 @@ async def test_async_update_cycle_timer_value_correct( [(86400, 1440), (1440, 24), (0, 0)], # minutes to seconds ) @pytest.mark.parametrize( - "setting", [PortSettingKey.CYCLE_DURATION_ON, PortSettingKey.CYCLE_DURATION_OFF] + "setting", [PortControlKey.CYCLE_DURATION_ON, PortControlKey.CYCLE_DURATION_OFF] ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_set_native_value_cycle_timer( @@ -328,7 +330,7 @@ async def test_async_set_native_value_cycle_timer( assert isinstance(entity, ACInfinityPortNumberEntity) await entity.async_set_native_value(field_value) - test_objects.port_set_mock.assert_called_with( + test_objects.port_control_set_mock.assert_called_with( str(DEVICE_ID), port, setting, expected ) test_objects.refresh_mock.assert_called() @@ -337,12 +339,12 @@ async def test_async_set_native_value_cycle_timer( "setting, f_setting", [ ( - PortSettingKey.AUTO_TEMP_HIGH_TRIGGER, - PortSettingKey.AUTO_TEMP_HIGH_TRIGGER_F, + PortControlKey.AUTO_TEMP_HIGH_TRIGGER, + PortControlKey.AUTO_TEMP_HIGH_TRIGGER_F, ), ( - PortSettingKey.AUTO_TEMP_LOW_TRIGGER, - PortSettingKey.AUTO_TEMP_LOW_TRIGGER_F, + PortControlKey.AUTO_TEMP_LOW_TRIGGER, + PortControlKey.AUTO_TEMP_LOW_TRIGGER_F, ), ], ) @@ -367,8 +369,8 @@ async def test_async_update_temp_trigger_correct( setup, async_setup_entry, port, setting ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][setting] = c - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][f_setting] = f + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][setting] = c + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][f_setting] = f entity._handle_coordinator_update() assert isinstance(entity, ACInfinityPortNumberEntity) @@ -383,12 +385,12 @@ async def test_async_update_temp_trigger_correct( "setting, f_setting", [ ( - PortSettingKey.AUTO_TEMP_HIGH_TRIGGER, - PortSettingKey.AUTO_TEMP_HIGH_TRIGGER_F, + PortControlKey.AUTO_TEMP_HIGH_TRIGGER, + PortControlKey.AUTO_TEMP_HIGH_TRIGGER_F, ), ( - PortSettingKey.AUTO_TEMP_LOW_TRIGGER, - PortSettingKey.AUTO_TEMP_LOW_TRIGGER_F, + PortControlKey.AUTO_TEMP_LOW_TRIGGER, + PortControlKey.AUTO_TEMP_LOW_TRIGGER_F, ), ], ) @@ -409,7 +411,7 @@ async def test_async_set_temp_trigger_value( assert isinstance(entity, ACInfinityPortNumberEntity) await entity.async_set_native_value(c) - test_objects.port_sets_mock.assert_called_with( + test_objects.port_control_sets_mock.assert_called_with( str(DEVICE_ID), port, [(setting, c), (f_setting, f)] ) test_objects.refresh_mock.assert_called() @@ -417,9 +419,8 @@ async def test_async_set_temp_trigger_value( @pytest.mark.parametrize( "setting", [ - ControllerSettingKey.CALIBRATE_TEMP, - ControllerSettingKey.VPD_LEAF_TEMP_OFFSET, - ControllerSettingKey.CALIBRATE_HUMIDITY, + AdvancedSettingsKey.CALIBRATE_TEMP, + AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET, ], ) @pytest.mark.parametrize("temp_unit,expected", [(0, 20), (1, 10)]) @@ -427,17 +428,14 @@ async def test_async_setup_entry_temp_calibration_created( self, setup, setting, temp_unit, expected ): """Sensor for device reported temperature is created on setup""" - if setting == ControllerSettingKey.CALIBRATE_HUMIDITY: - expected = 10 - test_objects: ACTestObjects = setup - test_objects.ac_infinity._controller_settings[str(DEVICE_ID)][ - ControllerSettingKey.TEMP_UNIT + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), 1)][ + AdvancedSettingsKey.TEMP_UNIT ] = temp_unit # reset statistics for description in CONTROLLER_DESCRIPTIONS: - if description.key != ControllerSettingKey.CALIBRATE_HUMIDITY: + if description.key != AdvancedSettingsKey.CALIBRATE_HUMIDITY: description.native_min_value = -20 description.native_max_value = 20 @@ -455,12 +453,32 @@ async def test_async_setup_entry_temp_calibration_created( assert entity.device_info is not None + async def test_async_setup_entry_humidity_calibration_created(self, setup): + """Sensor for device reported humidity is created on setup""" + + entity = await execute_and_get_controller_entity( + setup, async_setup_entry, AdvancedSettingsKey.CALIBRATE_HUMIDITY + ) + + assert entity.device_info is not None + + assert isinstance(entity, ACInfinityControllerNumberEntity) + assert ( + entity.unique_id + == f"{DOMAIN}_{MAC_ADDR}_{AdvancedSettingsKey.CALIBRATE_HUMIDITY}" + ) + assert entity.entity_description.device_class is None + assert entity.entity_description.native_min_value == -10 + assert entity.entity_description.native_max_value == 10 + + assert entity.device_info is not None + @pytest.mark.parametrize( "setting", [ - ControllerSettingKey.CALIBRATE_TEMP, - ControllerSettingKey.VPD_LEAF_TEMP_OFFSET, - ControllerSettingKey.CALIBRATE_HUMIDITY, + AdvancedSettingsKey.CALIBRATE_TEMP, + AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET, + AdvancedSettingsKey.CALIBRATE_HUMIDITY, ], ) @pytest.mark.parametrize("value", [-10, 0, 5]) @@ -472,7 +490,7 @@ async def test_async_update_calibration(self, setup, setting, value): setup, async_setup_entry, setting ) - test_objects.ac_infinity._controller_settings[str(DEVICE_ID)][setting] = value + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), 0)][setting] = value entity._handle_coordinator_update() assert isinstance(entity, ACInfinityControllerNumberEntity) @@ -500,12 +518,12 @@ async def test_async_set_native_value_temp_calibration( future.set_result(None) test_objects: ACTestObjects = setup - test_objects.ac_infinity._controller_settings[str(DEVICE_ID)][ - ControllerSettingKey.TEMP_UNIT + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), 0)][ + AdvancedSettingsKey.TEMP_UNIT ] = temp_unit entity = await execute_and_get_controller_entity( - setup, async_setup_entry, ControllerSettingKey.CALIBRATE_TEMP + setup, async_setup_entry, AdvancedSettingsKey.CALIBRATE_TEMP ) assert isinstance(entity, ACInfinityControllerNumberEntity) @@ -515,16 +533,16 @@ async def test_async_set_native_value_temp_calibration( test_objects.controller_sets_mock.assert_called_with( str(DEVICE_ID), [ - (ControllerSettingKey.CALIBRATE_TEMP, expected), - (ControllerSettingKey.CALIBRATE_TEMP_F, 0), + (AdvancedSettingsKey.CALIBRATE_TEMP, expected), + (AdvancedSettingsKey.CALIBRATE_TEMP_F, 0), ], ) else: test_objects.controller_sets_mock.assert_called_with( str(DEVICE_ID), [ - (ControllerSettingKey.CALIBRATE_TEMP, 0), - (ControllerSettingKey.CALIBRATE_TEMP_F, expected), + (AdvancedSettingsKey.CALIBRATE_TEMP, 0), + (AdvancedSettingsKey.CALIBRATE_TEMP_F, expected), ], ) test_objects.refresh_mock.assert_called() @@ -550,12 +568,12 @@ async def test_async_set_native_value_vpd_leaf_temp_calibration( future.set_result(None) test_objects: ACTestObjects = setup - test_objects.ac_infinity._controller_settings[str(DEVICE_ID)][ - ControllerSettingKey.TEMP_UNIT + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), 0)][ + AdvancedSettingsKey.TEMP_UNIT ] = temp_unit entity = await execute_and_get_controller_entity( - setup, async_setup_entry, ControllerSettingKey.VPD_LEAF_TEMP_OFFSET + setup, async_setup_entry, AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET ) assert isinstance(entity, ACInfinityControllerNumberEntity) @@ -563,9 +581,9 @@ async def test_async_set_native_value_vpd_leaf_temp_calibration( test_objects.controller_set_mock.assert_called_with( str(DEVICE_ID), - ControllerSettingKey.VPD_LEAF_TEMP_OFFSET + AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET if temp_unit > 0 - else ControllerSettingKey.VPD_LEAF_TEMP_OFFSET_F, + else AdvancedSettingsKey.VPD_LEAF_TEMP_OFFSET_F, expected, ) @@ -584,14 +602,242 @@ async def test_async_set_native_value_humidity_calibration( test_objects: ACTestObjects = setup entity = await execute_and_get_controller_entity( - setup, async_setup_entry, ControllerSettingKey.CALIBRATE_HUMIDITY + setup, async_setup_entry, AdvancedSettingsKey.CALIBRATE_HUMIDITY ) assert isinstance(entity, ACInfinityControllerNumberEntity) await entity.async_set_native_value(value) test_objects.controller_set_mock.assert_called_with( - str(DEVICE_ID), ControllerSettingKey.CALIBRATE_HUMIDITY, value + str(DEVICE_ID), AdvancedSettingsKey.CALIBRATE_HUMIDITY, value + ) + + test_objects.refresh_mock.assert_called() + + @pytest.mark.parametrize( + "setting", + [ + AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP, + AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP, + ], + ) + @pytest.mark.parametrize("temp_unit,expected", [(0, 20), (1, 10)]) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_setup_dynamic_temp_setup_for_each_port( + self, setup, setting: str, port, temp_unit, expected + ): + """Dynamic response temp controls setup for each port""" + test_objects: ACTestObjects = setup + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), port)][ + AdvancedSettingsKey.TEMP_UNIT + ] = temp_unit + + # reset statistics + for description in PORT_DESCRIPTIONS: + if description.key == setting: + description.native_max_value = 20 + + entity = await execute_and_get_port_entity( + setup, async_setup_entry, port, setting + ) + + assert entity.device_info is not None + + assert isinstance(entity, ACInfinityPortNumberEntity) + assert entity.unique_id == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{setting}" + assert entity.entity_description.device_class is None + assert entity.entity_description.native_min_value == 0 + assert entity.entity_description.native_max_value == expected + + assert entity.device_info is not None + + @pytest.mark.parametrize( + "setting,step,max_value", + [ + (AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, 1, 10), + (AdvancedSettingsKey.DYNAMIC_BUFFER_HUMIDITY, 1, 10), + (AdvancedSettingsKey.DYNAMIC_TRANSITION_VPD, 0.1, 1), + (AdvancedSettingsKey.DYNAMIC_BUFFER_VPD, 0.1, 1), + ], + ) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_setup_dynamic_humidity_vpd_setup_for_each_port( + self, setup, port, setting: str, step, max_value + ): + """Dynamic response temp controls setup for each port""" + entity = await execute_and_get_port_entity( + setup, async_setup_entry, port, setting + ) + + assert entity.device_info is not None + + assert isinstance(entity, ACInfinityPortNumberEntity) + assert entity.unique_id == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{setting}" + assert entity.entity_description.device_class is None + assert entity.entity_description.native_step == step + assert entity.entity_description.native_min_value == 0 + assert entity.entity_description.native_max_value == max_value + + assert entity.device_info is not None + + @pytest.mark.parametrize( + "setting,value,expected", + [ + (AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP, 8, 8), + (AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, 8, 8), + (AdvancedSettingsKey.DYNAMIC_TRANSITION_VPD, 8, 0.8), + (AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP, 8, 8), + (AdvancedSettingsKey.DYNAMIC_BUFFER_HUMIDITY, 8, 8), + (AdvancedSettingsKey.DYNAMIC_BUFFER_VPD, 8, 0.8), + ], + ) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_update_dynamic_response( + self, setup, setting: str, value, expected, port + ): + """Reported sensor value matches the value in the json payload""" + + test_objects: ACTestObjects = setup + entity = await execute_and_get_port_entity( + setup, async_setup_entry, port, setting + ) + + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), port)][ + setting + ] = value + entity._handle_coordinator_update() + + assert isinstance(entity, ACInfinityPortNumberEntity) + assert entity.native_value == expected + test_objects.write_ha_mock.assert_called() + + @pytest.mark.parametrize( + "setting,f_setting", + [ + ( + AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP, + AdvancedSettingsKey.DYNAMIC_TRANSITION_TEMP_F, + ), + ( + AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP, + AdvancedSettingsKey.DYNAMIC_BUFFER_TEMP_F, + ), + ], + ) + @pytest.mark.parametrize( + "temp_unit,value,f_expected,expected", + [ + # F max is 20 + (0, 0, 0, 0), + (0, 10, 10, 5), + (0, 11, 11, 5), + (0, 20, 20, 10), + # C max is 10 + (1, 0, 0, 0), + (1, 5, 10, 5), + (1, 6, 12, 6), + (1, 10, 20, 10), + (1, 20, 20, 10), + ], + ) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_set_native_value_dynamic_response_temp( + self, setup, temp_unit, value, expected, f_expected, setting, f_setting, port + ): + """Reported sensor value matches the value in the json payload""" + future: Future = asyncio.Future() + future.set_result(None) + + test_objects: ACTestObjects = setup + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), port)][ + AdvancedSettingsKey.TEMP_UNIT + ] = temp_unit + + entity = await execute_and_get_port_entity( + setup, async_setup_entry, port, setting + ) + + assert isinstance(entity, ACInfinityPortNumberEntity) + await entity.async_set_native_value(value) + + if temp_unit > 0: + test_objects.port_setting_sets_mock.assert_called_with( + str(DEVICE_ID), + port, + [ + (setting, expected), + (f_setting, f_expected), + ], + ) + else: + test_objects.port_setting_sets_mock.assert_called_with( + str(DEVICE_ID), + port, + [ + (setting, expected), + (f_setting, f_expected), + ], + ) + test_objects.refresh_mock.assert_called() + + @pytest.mark.parametrize( + "setting", + [ + AdvancedSettingsKey.DYNAMIC_TRANSITION_HUMIDITY, + AdvancedSettingsKey.DYNAMIC_BUFFER_HUMIDITY, + ], + ) + @pytest.mark.parametrize("value", [0, 5, 10]) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_set_native_value_dynamic_response_humidity( + self, setup, value, port, setting + ): + """Reported sensor value matches the value in the json payload""" + future: Future = asyncio.Future() + future.set_result(None) + + test_objects: ACTestObjects = setup + + entity = await execute_and_get_port_entity( + setup, async_setup_entry, port, setting + ) + + assert isinstance(entity, ACInfinityPortNumberEntity) + await entity.async_set_native_value(value) + + test_objects.port_setting_set_mock.assert_called_with( + str(DEVICE_ID), port, setting, value + ) + + test_objects.refresh_mock.assert_called() + + @pytest.mark.parametrize( + "setting", + [ + AdvancedSettingsKey.DYNAMIC_TRANSITION_VPD, + AdvancedSettingsKey.DYNAMIC_BUFFER_VPD, + ], + ) + @pytest.mark.parametrize("expected,value", [(0, 0), (5, 0.5), (10, 1)]) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_set_native_value_dynamic_response_vpd( + self, setup, value, expected, port, setting + ): + """Reported sensor value matches the value in the json payload""" + future: Future = asyncio.Future() + future.set_result(None) + + test_objects: ACTestObjects = setup + + entity = await execute_and_get_port_entity( + setup, async_setup_entry, port, setting + ) + + assert isinstance(entity, ACInfinityPortNumberEntity) + await entity.async_set_native_value(value) + + test_objects.port_setting_set_mock.assert_called_with( + str(DEVICE_ID), port, setting, expected ) test_objects.refresh_mock.assert_called() diff --git a/tests/test_select.py b/tests/test_select.py index 42612b1..c4511c2 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -6,7 +6,8 @@ from custom_components.ac_infinity.const import ( DOMAIN, - PortSettingKey, + AdvancedSettingsKey, + PortControlKey, ) from custom_components.ac_infinity.select import ( ACInfinityPortSelectEntity, @@ -26,7 +27,7 @@ def setup(mocker: MockFixture): @pytest.mark.asyncio -class TestNumbers: +class TestSelectors: set_data_mode_value = 0 async def test_async_setup_all_sensors_created(self, setup): @@ -39,27 +40,30 @@ async def test_async_setup_all_sensors_created(self, setup): test_objects.entities.add_entities_callback, ) - assert len(test_objects.entities._added_entities) == 4 + assert len(test_objects.entities._added_entities) == 8 + @pytest.mark.parametrize( + "setting,option_count", + [(PortControlKey.AT_TYPE, 8), (AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, 2)], + ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) - async def test_async_setup_mode_created_for_each_port(self, setup, port): + async def test_async_setup_mode_created_for_each_port( + self, setup, port, setting, option_count + ): """Sensor for device port mode created on setup""" entity = await execute_and_get_port_entity( setup, async_setup_entry, port, - PortSettingKey.AT_TYPE, + setting, ) assert isinstance(entity, ACInfinityPortSelectEntity) - assert ( - entity.unique_id - == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{PortSettingKey.AT_TYPE}" - ) + assert entity.unique_id == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{setting}" assert entity.entity_description.options is not None - assert len(entity.entity_description.options) == 8 + assert len(entity.entity_description.options) == option_count assert entity.device_info is not None @pytest.mark.parametrize( @@ -86,11 +90,11 @@ async def test_async_update_mode_value_correct( setup, async_setup_entry, port, - PortSettingKey.AT_TYPE, + PortControlKey.AT_TYPE, ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][ - PortSettingKey.AT_TYPE + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][ + PortControlKey.AT_TYPE ] = at_type entity._handle_coordinator_update() @@ -112,7 +116,70 @@ async def test_async_update_mode_value_correct( ], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) - async def test_async_set_native_value(self, setup, at_type_string, expected, port): + async def test_async_set_native_value_at_type( + self, setup, at_type_string, expected, port + ): + """Reported sensor value matches the value in the json payload""" + future: Future = asyncio.Future() + future.set_result(None) + + test_objects: ACTestObjects = setup + entity = await execute_and_get_port_entity( + setup, + async_setup_entry, + port, + PortControlKey.AT_TYPE, + ) + + assert isinstance(entity, ACInfinityPortSelectEntity) + await entity.async_select_option(at_type_string) + + test_objects.port_control_set_mock.assert_called_with( + str(DEVICE_ID), port, PortControlKey.AT_TYPE, expected + ) + test_objects.refresh_mock.assert_called() + + @pytest.mark.parametrize( + "value,expected", + [ + (0, "Transition"), + (1, "Buffer"), + ], + ) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_update_dynamic_response_value_correct( + self, setup, value, expected, port + ): + """Reported sensor value matches the value in the json payload""" + + test_objects: ACTestObjects = setup + entity = await execute_and_get_port_entity( + setup, + async_setup_entry, + port, + AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, + ) + + test_objects.ac_infinity._device_settings[(str(DEVICE_ID), port)][ + AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE + ] = value + entity._handle_coordinator_update() + + assert isinstance(entity, ACInfinityPortSelectEntity) + assert entity.current_option == expected + test_objects.write_ha_mock.assert_called() + + @pytest.mark.parametrize( + "expected,at_type_string", + [ + (0, "Transition"), + (1, "Buffer"), + ], + ) + @pytest.mark.parametrize("port", [1, 2, 3, 4]) + async def test_async_set_native_value_dynamic_response( + self, setup, at_type_string, expected, port + ): """Reported sensor value matches the value in the json payload""" future: Future = asyncio.Future() future.set_result(None) @@ -122,13 +189,13 @@ async def test_async_set_native_value(self, setup, at_type_string, expected, por setup, async_setup_entry, port, - PortSettingKey.AT_TYPE, + AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, ) assert isinstance(entity, ACInfinityPortSelectEntity) await entity.async_select_option(at_type_string) - test_objects.port_set_mock.assert_called_with( - str(DEVICE_ID), port, PortSettingKey.AT_TYPE, expected + test_objects.port_setting_set_mock.assert_called_with( + str(DEVICE_ID), port, AdvancedSettingsKey.DYNAMIC_RESPONSE_TYPE, expected ) test_objects.refresh_mock.assert_called() diff --git a/tests/test_sensor.py b/tests/test_sensor.py index fd21b08..acc753a 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -10,8 +10,8 @@ from custom_components.ac_infinity.const import ( DOMAIN, ControllerPropertyKey, + PortControlKey, PortPropertyKey, - PortSettingKey, ) from custom_components.ac_infinity.sensor import ( ACInfinityControllerSensorEntity, @@ -155,12 +155,12 @@ async def test_async_setup_remaining_time_for_each_port(self, setup, port): """Sensor for device port surplus created on setup""" entity = await execute_and_get_port_entity( - setup, async_setup_entry, port, PortSettingKey.SURPLUS + setup, async_setup_entry, port, PortControlKey.SURPLUS ) assert ( entity.unique_id - == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{PortSettingKey.SURPLUS}" + == f"{DOMAIN}_{MAC_ADDR}_port_{port}_{PortControlKey.SURPLUS}" ) assert entity.entity_description.device_class == SensorDeviceClass.DURATION assert entity.device_info is not None @@ -179,7 +179,7 @@ async def test_async_update_current_power_value_correct( ): """Reported sensor value matches the value in the json payload""" test_objects: ACTestObjects = setup - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][ + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][ PortPropertyKey.SPEAK ] = expected entity = await execute_and_get_port_entity( @@ -199,11 +199,11 @@ async def test_async_update_duration_left_value_correct( test_objects: ACTestObjects = setup entity = await execute_and_get_port_entity( - setup, async_setup_entry, port, PortSettingKey.SURPLUS + setup, async_setup_entry, port, PortControlKey.SURPLUS ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][ - PortSettingKey.SURPLUS + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][ + PortControlKey.SURPLUS ] = value entity._handle_coordinator_update() diff --git a/tests/test_switch.py b/tests/test_switch.py index 2cdb3b7..7b4350e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -4,7 +4,7 @@ import pytest from pytest_mock import MockFixture -from custom_components.ac_infinity.const import DOMAIN, PortSettingKey +from custom_components.ac_infinity.const import DOMAIN, PortControlKey from custom_components.ac_infinity.switch import ( SCHEDULE_DISABLED_VALUE, SCHEDULE_EOD_VALUE, @@ -26,7 +26,7 @@ def setup(mocker: MockFixture): @pytest.mark.asyncio -class TestSwitch: +class TestSwitches: async def test_async_setup_all_sensors_created(self, setup): """All sensors created""" test_objects: ACTestObjects = setup @@ -42,14 +42,14 @@ async def test_async_setup_all_sensors_created(self, setup): @pytest.mark.parametrize( "setting", [ - PortSettingKey.AUTO_HUMIDITY_HIGH_ENABLED, - PortSettingKey.AUTO_HUMIDITY_LOW_ENABLED, - PortSettingKey.AUTO_TEMP_HIGH_ENABLED, - PortSettingKey.AUTO_TEMP_LOW_ENABLED, - PortSettingKey.VPD_HIGH_ENABLED, - PortSettingKey.VPD_LOW_ENABLED, - PortSettingKey.SCHEDULED_START_TIME, - PortSettingKey.SCHEDULED_END_TIME, + PortControlKey.AUTO_HUMIDITY_HIGH_ENABLED, + PortControlKey.AUTO_HUMIDITY_LOW_ENABLED, + PortControlKey.AUTO_TEMP_HIGH_ENABLED, + PortControlKey.AUTO_TEMP_LOW_ENABLED, + PortControlKey.VPD_HIGH_ENABLED, + PortControlKey.VPD_LOW_ENABLED, + PortControlKey.SCHEDULED_START_TIME, + PortControlKey.SCHEDULED_END_TIME, ], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) @@ -70,23 +70,23 @@ async def test_async_setup_mode_created_for_each_port(self, setup, port, setting "setting,value,expected", [ # enabled - (PortSettingKey.AUTO_HUMIDITY_HIGH_ENABLED, 1, True), - (PortSettingKey.AUTO_HUMIDITY_LOW_ENABLED, 1, True), - (PortSettingKey.AUTO_TEMP_HIGH_ENABLED, 1, True), - (PortSettingKey.AUTO_TEMP_LOW_ENABLED, 1, True), - (PortSettingKey.VPD_HIGH_ENABLED, 1, True), - (PortSettingKey.VPD_LOW_ENABLED, 1, True), - (PortSettingKey.SCHEDULED_START_TIME, SCHEDULE_MIDNIGHT_VALUE, True), - (PortSettingKey.SCHEDULED_END_TIME, SCHEDULE_MIDNIGHT_VALUE, True), + (PortControlKey.AUTO_HUMIDITY_HIGH_ENABLED, 1, True), + (PortControlKey.AUTO_HUMIDITY_LOW_ENABLED, 1, True), + (PortControlKey.AUTO_TEMP_HIGH_ENABLED, 1, True), + (PortControlKey.AUTO_TEMP_LOW_ENABLED, 1, True), + (PortControlKey.VPD_HIGH_ENABLED, 1, True), + (PortControlKey.VPD_LOW_ENABLED, 1, True), + (PortControlKey.SCHEDULED_START_TIME, SCHEDULE_MIDNIGHT_VALUE, True), + (PortControlKey.SCHEDULED_END_TIME, SCHEDULE_MIDNIGHT_VALUE, True), # disabled - (PortSettingKey.AUTO_HUMIDITY_HIGH_ENABLED, 0, False), - (PortSettingKey.AUTO_HUMIDITY_LOW_ENABLED, 0, False), - (PortSettingKey.AUTO_TEMP_HIGH_ENABLED, 0, False), - (PortSettingKey.AUTO_TEMP_LOW_ENABLED, 0, False), - (PortSettingKey.VPD_HIGH_ENABLED, 0, False), - (PortSettingKey.VPD_LOW_ENABLED, 0, False), - (PortSettingKey.SCHEDULED_START_TIME, SCHEDULE_DISABLED_VALUE, False), - (PortSettingKey.SCHEDULED_END_TIME, SCHEDULE_DISABLED_VALUE, False), + (PortControlKey.AUTO_HUMIDITY_HIGH_ENABLED, 0, False), + (PortControlKey.AUTO_HUMIDITY_LOW_ENABLED, 0, False), + (PortControlKey.AUTO_TEMP_HIGH_ENABLED, 0, False), + (PortControlKey.AUTO_TEMP_LOW_ENABLED, 0, False), + (PortControlKey.VPD_HIGH_ENABLED, 0, False), + (PortControlKey.VPD_LOW_ENABLED, 0, False), + (PortControlKey.SCHEDULED_START_TIME, SCHEDULE_DISABLED_VALUE, False), + (PortControlKey.SCHEDULED_END_TIME, SCHEDULE_DISABLED_VALUE, False), ], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) @@ -103,7 +103,7 @@ async def test_async_update_mode_value_correct( setting, ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][setting] = value + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][setting] = value entity._handle_coordinator_update() assert isinstance(entity, ACInfinityPortSwitchEntity) @@ -114,14 +114,14 @@ async def test_async_update_mode_value_correct( "setting,expected", [ # enabled - (PortSettingKey.AUTO_HUMIDITY_HIGH_ENABLED, 1), - (PortSettingKey.AUTO_HUMIDITY_LOW_ENABLED, 1), - (PortSettingKey.AUTO_TEMP_HIGH_ENABLED, 1), - (PortSettingKey.AUTO_TEMP_LOW_ENABLED, 1), - (PortSettingKey.VPD_HIGH_ENABLED, 1), - (PortSettingKey.VPD_LOW_ENABLED, 1), - (PortSettingKey.SCHEDULED_START_TIME, SCHEDULE_MIDNIGHT_VALUE), - (PortSettingKey.SCHEDULED_END_TIME, SCHEDULE_EOD_VALUE), + (PortControlKey.AUTO_HUMIDITY_HIGH_ENABLED, 1), + (PortControlKey.AUTO_HUMIDITY_LOW_ENABLED, 1), + (PortControlKey.AUTO_TEMP_HIGH_ENABLED, 1), + (PortControlKey.AUTO_TEMP_LOW_ENABLED, 1), + (PortControlKey.VPD_HIGH_ENABLED, 1), + (PortControlKey.VPD_LOW_ENABLED, 1), + (PortControlKey.SCHEDULED_START_TIME, SCHEDULE_MIDNIGHT_VALUE), + (PortControlKey.SCHEDULED_END_TIME, SCHEDULE_EOD_VALUE), ], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) @@ -141,7 +141,7 @@ async def test_async_turn_on(self, setup, expected, port, setting: str): assert isinstance(entity, ACInfinityPortSwitchEntity) await entity.async_turn_on() - test_objects.port_set_mock.assert_called_with( + test_objects.port_control_set_mock.assert_called_with( str(DEVICE_ID), port, setting, expected ) test_objects.refresh_mock.assert_called() @@ -149,14 +149,14 @@ async def test_async_turn_on(self, setup, expected, port, setting: str): @pytest.mark.parametrize( "setting,expected", [ - (PortSettingKey.AUTO_HUMIDITY_HIGH_ENABLED, 0), - (PortSettingKey.AUTO_HUMIDITY_LOW_ENABLED, 0), - (PortSettingKey.AUTO_TEMP_HIGH_ENABLED, 0), - (PortSettingKey.AUTO_TEMP_LOW_ENABLED, 0), - (PortSettingKey.VPD_HIGH_ENABLED, 0), - (PortSettingKey.VPD_LOW_ENABLED, 0), - (PortSettingKey.SCHEDULED_START_TIME, SCHEDULE_DISABLED_VALUE), - (PortSettingKey.SCHEDULED_END_TIME, SCHEDULE_DISABLED_VALUE), + (PortControlKey.AUTO_HUMIDITY_HIGH_ENABLED, 0), + (PortControlKey.AUTO_HUMIDITY_LOW_ENABLED, 0), + (PortControlKey.AUTO_TEMP_HIGH_ENABLED, 0), + (PortControlKey.AUTO_TEMP_LOW_ENABLED, 0), + (PortControlKey.VPD_HIGH_ENABLED, 0), + (PortControlKey.VPD_LOW_ENABLED, 0), + (PortControlKey.SCHEDULED_START_TIME, SCHEDULE_DISABLED_VALUE), + (PortControlKey.SCHEDULED_END_TIME, SCHEDULE_DISABLED_VALUE), ], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) @@ -176,7 +176,7 @@ async def test_async_turn_off(self, setup, expected, port, setting: str): assert isinstance(entity, ACInfinityPortSwitchEntity) await entity.async_turn_off() - test_objects.port_set_mock.assert_called_with( + test_objects.port_control_set_mock.assert_called_with( str(DEVICE_ID), port, setting, expected ) test_objects.refresh_mock.assert_called() diff --git a/tests/test_time.py b/tests/test_time.py index d1dd272..9ddfd56 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -8,7 +8,7 @@ from custom_components.ac_infinity.const import ( DOMAIN, SCHEDULE_DISABLED_VALUE, - PortSettingKey, + PortControlKey, ) from custom_components.ac_infinity.time import ( ACInfinityPortTimeEntity, @@ -24,7 +24,7 @@ def setup(mocker: MockFixture): @pytest.mark.asyncio -class TestTime: +class TestTimes: set_data_mode_value = 0 async def test_async_setup_all_sensors_created(self, setup): @@ -40,7 +40,7 @@ async def test_async_setup_all_sensors_created(self, setup): assert len(test_objects.entities._added_entities) == 8 @pytest.mark.parametrize( - "key", [PortSettingKey.SCHEDULED_START_TIME, PortSettingKey.SCHEDULED_END_TIME] + "key", [PortControlKey.SCHEDULED_START_TIME, PortControlKey.SCHEDULED_END_TIME] ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_setup_schedule_end_time_created_for_each_port( @@ -55,7 +55,7 @@ async def test_async_setup_schedule_end_time_created_for_each_port( @pytest.mark.parametrize( "setting", - [PortSettingKey.SCHEDULED_START_TIME, PortSettingKey.SCHEDULED_END_TIME], + [PortControlKey.SCHEDULED_START_TIME, PortControlKey.SCHEDULED_END_TIME], ) @pytest.mark.parametrize( "value,expected_hour,expected_minute", @@ -78,7 +78,7 @@ async def test_async_update_value_correct( setup, async_setup_entry, port, setting ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][setting] = value + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][setting] = value entity._handle_coordinator_update() assert isinstance(entity, ACInfinityPortTimeEntity) @@ -89,7 +89,7 @@ async def test_async_update_value_correct( @pytest.mark.parametrize( "setting", - [PortSettingKey.SCHEDULED_START_TIME, PortSettingKey.SCHEDULED_END_TIME], + [PortControlKey.SCHEDULED_START_TIME, PortControlKey.SCHEDULED_END_TIME], ) @pytest.mark.parametrize("value", [None, 1441, 65535]) @pytest.mark.parametrize("port", [1, 2, 3, 4]) @@ -103,7 +103,7 @@ async def test_async_update_value_represents_disabled_correct( setup, async_setup_entry, port, setting ) - test_objects.ac_infinity._port_settings[(str(DEVICE_ID), port)][setting] = value + test_objects.ac_infinity._port_controls[(str(DEVICE_ID), port)][setting] = value entity._handle_coordinator_update() assert isinstance(entity, ACInfinityPortTimeEntity) @@ -116,7 +116,7 @@ async def test_async_update_value_represents_disabled_correct( ) @pytest.mark.parametrize( "setting", - [PortSettingKey.SCHEDULED_START_TIME, PortSettingKey.SCHEDULED_END_TIME], + [PortControlKey.SCHEDULED_START_TIME, PortControlKey.SCHEDULED_END_TIME], ) @pytest.mark.parametrize("port", [1, 2, 3, 4]) async def test_async_set_native_value( @@ -135,7 +135,7 @@ async def test_async_set_native_value( assert isinstance(entity, ACInfinityPortTimeEntity) await entity.async_set_value(value) - test_objects.port_set_mock.assert_called_with( + test_objects.port_control_set_mock.assert_called_with( str(DEVICE_ID), port, setting, expected ) test_objects.refresh_mock.assert_called()