Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The integration polls the AC Infinity cloud API and exposes device state, enviro
```bash
docker-compose up homeassistant
```
This starts a Home Assistant instance with the custom_components directory volume-mounted, allowing you to test changes in real-time.
This starts a Home Assistant instance with the custom_components directory volume-mounted, allowing you to test changes in real-time. Access Home Assistant at `http://localhost:8123`

### IDE Configuration

Expand Down Expand Up @@ -197,6 +197,79 @@ The integration supports two distinct API patterns:
| Standard | `is_ai_controller == False` | `update_device_settings` |
| AI | `is_ai_controller == True` | `update_ai_device_control_and_settings` |

### Mutually Exclusive Sensor Pairs

Some physical sensors on AI+ controllers report a single measurement in one of two units depending on device configuration (e.g. Fahrenheit vs. Celsius, µS/cm vs. mS/cm). The API exposes these as separate `sensorType` integers, but they should map to a **single Home Assistant entity** whose unit reflects whichever type the device is actually reporting.

This is achieved by giving both `SensorType` entries the **same `SensorReferenceKey`** in `SENSOR_DESCRIPTIONS`. Because the entity unique ID is derived from the reference key (not the sensor type), only one entity is ever registered for a given sensor port — whichever type is present in the sensor array passes the `suitable_fn` check, and the other is silently ignored.

**Known mutually exclusive pairs:**

| Sensor | Type A | Type B |
|--------|--------|--------|
| Temperature (probe / controller) | `PROBE_TEMPERATURE_F` (0) / `CONTROLLER_TEMPERATURE_F` (4) | `PROBE_TEMPERATURE_C` (1) / `CONTROLLER_TEMPERATURE_C` (5) |
| Hydro water temperature | `HYDRO_WATER_TEMPERATURE_F` (18) | `HYDRO_WATER_TEMPERATURE_C` (19) |
| Hydro EC | `HYDRO_EC_US` (14) — µS/cm | `HYDRO_EC_MS` (15) — mS/cm |
| Hydro TDS | `HYDRO_TDS_PPM` (16) — ppm | `HYDRO_TDS_PPT` (17) — ppt |

**Example — adding a new mutually exclusive pair:**

1. Add both sensor type constants to `SensorType` in `const.py`:
```python
class SensorType:
MY_SENSOR_UNIT_A = 42
MY_SENSOR_UNIT_B = 43
```

2. Add a **single** shared reference key to `SensorReferenceKey` in `const.py`:
```python
class SensorReferenceKey:
MY_SENSOR = "mySensor"
```

3. Register **both** types in `SENSOR_DESCRIPTIONS` in `sensor.py` using the **same** `key` and `translation_key`:
```python
SensorType.MY_SENSOR_UNIT_A: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.MY_SENSOR,
native_unit_of_measurement="unit_a",
translation_key="my_sensor",
...
),
SensorType.MY_SENSOR_UNIT_B: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.MY_SENSOR,
native_unit_of_measurement="unit_b",
translation_key="my_sensor",
...
),
```

Only the type present in the device's sensor array at runtime will produce a live entity. If the user later reconfigures the device to report the other unit, a Home Assistant reload is required to swap the entity.

### Sensor Device Grouping

Each physical USB-C sensor accessory is represented as a child **device** in Home Assistant (visible under Settings → Devices). The grouping is determined in `ACInfinitySensor.__get_device_info` in `core.py`, which maps `sensorType` integers to a `DeviceInfo` object. Multiple sensor types from the same physical hardware share one `DeviceInfo` — keyed by a stable identifier built from `controller_id`, `sensor_port`, and a short hardware slug.

**Known sensor device groupings:**

| Physical Device | Model | Sensor Types |
|-----------------|-------|--------------|
| Probe Sensor | UIS Controller Sensor Probe (AC-SPC24) | `PROBE_TEMPERATURE_F/C` (0/1), `PROBE_HUMIDITY` (2), `PROBE_VPD` (3) |
| CO2 + Light Sensor | UIS CO2 + Light Sensor (AC-COS3) | `CO2` (11), `LIGHT` (12) |
| Water Sensor | UIS Water Sensor (AC-WDS3) | `WATER` (20) |
| Soil Sensor | UIS Soil Sensor (AC-SLS3) | `SOIL` (10) |
| Hydro Sensor | UIS Hydro Sensor (AC-HDS3) | `HYDRO_PH` (13), `HYDRO_EC_US/MS` (14/15), `HYDRO_TDS_PPM/PPT` (16/17), `HYDRO_WATER_TEMPERATURE_F/C` (18/19) |
| Controller (built-in) | — (returns the controller's own `DeviceInfo`) | `CONTROLLER_TEMPERATURE_F/C` (4/5), `CONTROLLER_HUMIDITY` (6), `CONTROLLER_VPD` (7) |

Controller-type sensor readings (types 4–7) do not create a child device — they attach directly to the controller device itself.

Unknown sensor types fall through to a catch-all that creates a generic `"Unknown Sensor"` device and logs a warning with a link to open a GitHub issue.

**When adding a new physical sensor device:**

1. Add the `SensorType` constants in `const.py`.
2. Add a new `case` block in `ACInfinitySensor.__get_device_info` in `core.py`, grouping all types that come from the same hardware under one `DeviceInfo`. The identifier slug should be lowercase and match the device's model suffix (e.g. `_hds3` for AC-HDS3).
3. Register the sensor types in `SENSOR_DESCRIPTIONS` in `sensor.py`.

### Entity Description Pattern

Each platform uses frozen dataclasses with mixins:
Expand Down Expand Up @@ -306,5 +379,3 @@ GitHub Actions runs:
- `pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term` for testing and coverage collection
- `pyright` for type checking
- Qodana scan and quality gate to block PRs when conditions are not met

Access Home Assistant at `http://localhost:8123`
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# homeassistant-acinfinity

[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)
[![Tests](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/tests.yaml/badge.svg)](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/tests.yaml)

[![HACS/HASS](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/validate.yaml/badge.svg)](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/validate.yaml)
[![CodeQL](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/codeql.yaml/badge.svg)](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/codeql.yaml)


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.

Expand Down
14 changes: 14 additions & 0 deletions custom_components/ac_infinity/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class MdiIcon:
POWER_PLUG = "mdi:power-plug"
WAVES = "mdi:waves"
POWER = "mdi:power"
PH = "mdi:ph"
SINE_WAVE = "mdi:sine-wave"
WATER_OPACITY = "mdi:water-opacity"


class ConfigurationKey:
Expand Down Expand Up @@ -120,6 +123,13 @@ class SensorType:
SOIL = 10
CO2 = 11
LIGHT = 12
HYDRO_PH = 13
HYDRO_EC_US = 14
HYDRO_EC_MS = 15
HYDRO_TDS_PPM = 16
HYDRO_TDS_PPT = 17
HYDRO_WATER_TEMPERATURE_F = 18
HYDRO_WATER_TEMPERATURE_C = 19
WATER = 20


Expand All @@ -136,6 +146,10 @@ class SensorReferenceKey:
LIGHT_SENSOR = "lightSensor"
WATER = "waterSensor"
SOIL = "soilSensor"
HYDRO_PH = "hydroPh"
HYDRO_EC = "hydroEc"
HYDRO_TDS = "hydroTds"
HYDRO_WATER_TEMPERATURE = "hydroWaterTemperature"


# noinspection SpellCheckingInspection
Expand Down
18 changes: 18 additions & 0 deletions custom_components/ac_infinity/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,24 @@ def __get_device_info(
via_device=controller.identifier,
model="UIS Soil Sensor (AC-SLS3)",
)
case (
SensorType.HYDRO_PH
| SensorType.HYDRO_EC_US
| SensorType.HYDRO_EC_MS
| SensorType.HYDRO_TDS_PPM
| SensorType.HYDRO_TDS_PPT
| SensorType.HYDRO_WATER_TEMPERATURE_F
| SensorType.HYDRO_WATER_TEMPERATURE_C
):
return DeviceInfo(
identifiers={
(DOMAIN, f"{controller.controller_id}_{sensor_port}_hds3")
},
name=f"{controller.controller_name} Hydro Sensor",
manufacturer=MANUFACTURER,
via_device=controller.identifier,
model="UIS Hydro Sensor (AC-HDS3)",
)
case (
SensorType.CONTROLLER_TEMPERATURE_F
| SensorType.CONTROLLER_TEMPERATURE_C
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ac_infinity/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/dalinicus/homeassistant-acinfinity",
"requirements": [],
"version": "2.1.1"
"version": "2.2.0"
}
85 changes: 85 additions & 0 deletions custom_components/ac_infinity/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
Platform,
UnitOfConductivity,
UnitOfPressure,
UnitOfTemperature,
UnitOfTime,
Expand Down Expand Up @@ -406,6 +407,90 @@ def __get_next_mode_change_timestamp(
suitable_fn=__suitable_fn_sensor_default,
get_value_fn=__get_value_fn_sensor_value_default,
),
SensorType.HYDRO_PH: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.HYDRO_PH,
device_class=SensorDeviceClass.PH,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=None,
suggested_unit_of_measurement=None,
icon=MdiIcon.PH,
translation_key="hydro_ph_sensor",
enabled_fn=enabled_fn_sensor,
suitable_fn=__suitable_fn_sensor_default,
get_value_fn=__get_value_fn_sensor_value_default,
),
SensorType.HYDRO_EC_US: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.HYDRO_EC,
device_class=SensorDeviceClass.CONDUCTIVITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS_PER_CM,
suggested_unit_of_measurement=None,
icon=MdiIcon.SINE_WAVE,
translation_key="hydro_ec_sensor",
enabled_fn=enabled_fn_sensor,
suitable_fn=__suitable_fn_sensor_default,
get_value_fn=__get_value_fn_sensor_value_default,
),
SensorType.HYDRO_EC_MS: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.HYDRO_EC,
device_class=SensorDeviceClass.CONDUCTIVITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfConductivity.MILLISIEMENS_PER_CM,
suggested_unit_of_measurement=None,
icon=MdiIcon.SINE_WAVE,
translation_key="hydro_ec_sensor",
enabled_fn=enabled_fn_sensor,
suitable_fn=__suitable_fn_sensor_default,
get_value_fn=__get_value_fn_sensor_value_default,
),
SensorType.HYDRO_TDS_PPM: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.HYDRO_TDS,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
suggested_unit_of_measurement=None,
icon=MdiIcon.WATER_OPACITY,
translation_key="hydro_tds_sensor",
enabled_fn=enabled_fn_sensor,
suitable_fn=__suitable_fn_sensor_default,
get_value_fn=__get_value_fn_sensor_value_default,
),
SensorType.HYDRO_TDS_PPT: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.HYDRO_TDS,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="ppt",
suggested_unit_of_measurement=None,
icon=MdiIcon.WATER_OPACITY,
translation_key="hydro_tds_sensor",
enabled_fn=enabled_fn_sensor,
suitable_fn=__suitable_fn_sensor_default,
get_value_fn=__get_value_fn_sensor_value_default,
),
SensorType.HYDRO_WATER_TEMPERATURE_F: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.HYDRO_WATER_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=None,
icon=MdiIcon.WATER_THERMOMETER_OUTLINE,
translation_key="hydro_water_temperature_sensor",
enabled_fn=enabled_fn_sensor,
suitable_fn=__suitable_fn_sensor_temperature,
get_value_fn=__get_value_fn_sensor_value_temperature,
),
SensorType.HYDRO_WATER_TEMPERATURE_C: ACInfinitySensorSensorEntityDescription(
key=SensorReferenceKey.HYDRO_WATER_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=None,
icon=MdiIcon.WATER_THERMOMETER_OUTLINE,
translation_key="hydro_water_temperature_sensor",
enabled_fn=enabled_fn_sensor,
suitable_fn=__suitable_fn_sensor_temperature,
get_value_fn=__get_value_fn_sensor_value_temperature,
),
}

DEVICE_DESCRIPTIONS: list[ACInfinityDeviceSensorEntityDescription] = [
Expand Down
12 changes: 12 additions & 0 deletions custom_components/ac_infinity/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,18 @@
"soil_sensor": {
"name": "Soil Moisture"
},
"hydro_ph_sensor": {
"name": "pH"
},
"hydro_ec_sensor": {
"name": "EC"
},
"hydro_tds_sensor": {
"name": "TDS"
},
"hydro_water_temperature_sensor": {
"name": "Water Temperature"
},
"current_power": {
"name": "Current Power"
},
Expand Down
12 changes: 12 additions & 0 deletions custom_components/ac_infinity/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,18 @@
"soil_sensor": {
"name": "Soil Moisture"
},
"hydro_ph_sensor": {
"name": "pH"
},
"hydro_ec_sensor": {
"name": "EC"
},
"hydro_tds_sensor": {
"name": "TDS"
},
"hydro_water_temperature_sensor": {
"name": "Water Temperature"
},
"current_power": {
"name": "Current Power"
},
Expand Down
Loading
Loading