├── .gitignore
├── hacs.json
├── images
└── screenshot.png
├── logos
├── tfa-logo-2021-256.png
├── tfa-logo-2021-512.png
├── dark_tfa-logo-2021-256.png
├── dark_tfa-logo-2021-512.png
├── logo_tfa-logo-2021_256.png
├── logo_tfa-logo-2021_512.png
├── dark_logo_tfa-logo-2021_256.png
├── dark_logo_tfa-logo-2021_512.png
├── logo-white.svg
└── tfa-logo-2021.svg
├── resources
└── AN135-CO2mini-usb-protocol.pdf
├── .github
└── workflows
│ ├── Integration_Validation.yml
│ └── HACS_Validation.yml
├── custom_components
└── airco2ntrol
│ ├── manifest.json
│ ├── config_flow.py
│ ├── __init__.py
│ ├── sensor_reader.py
│ └── sensor.py
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /__pycache__
3 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "airco2ntrol"
3 | }
--------------------------------------------------------------------------------
/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/images/screenshot.png
--------------------------------------------------------------------------------
/logos/tfa-logo-2021-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/tfa-logo-2021-256.png
--------------------------------------------------------------------------------
/logos/tfa-logo-2021-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/tfa-logo-2021-512.png
--------------------------------------------------------------------------------
/logos/dark_tfa-logo-2021-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/dark_tfa-logo-2021-256.png
--------------------------------------------------------------------------------
/logos/dark_tfa-logo-2021-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/dark_tfa-logo-2021-512.png
--------------------------------------------------------------------------------
/logos/logo_tfa-logo-2021_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/logo_tfa-logo-2021_256.png
--------------------------------------------------------------------------------
/logos/logo_tfa-logo-2021_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/logo_tfa-logo-2021_512.png
--------------------------------------------------------------------------------
/logos/dark_logo_tfa-logo-2021_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/dark_logo_tfa-logo-2021_256.png
--------------------------------------------------------------------------------
/logos/dark_logo_tfa-logo-2021_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/logos/dark_logo_tfa-logo-2021_512.png
--------------------------------------------------------------------------------
/resources/AN135-CO2mini-usb-protocol.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leorbs/airco2ntrol/HEAD/resources/AN135-CO2mini-usb-protocol.pdf
--------------------------------------------------------------------------------
/.github/workflows/Integration_Validation.yml:
--------------------------------------------------------------------------------
1 | name: Validate with hassfest
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: '0 0 * * *'
8 |
9 | jobs:
10 | validate:
11 | runs-on: "ubuntu-latest"
12 | steps:
13 | - uses: "actions/checkout@v4"
14 | - uses: "home-assistant/actions/hassfest@master"
--------------------------------------------------------------------------------
/.github/workflows/HACS_Validation.yml:
--------------------------------------------------------------------------------
1 | name: HACS Action
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 |
9 | jobs:
10 | hacs:
11 | name: HACS Action
12 | runs-on: "ubuntu-latest"
13 | steps:
14 | - name: HACS Action
15 | uses: "hacs/action@main"
16 | with:
17 | category: "integration"
--------------------------------------------------------------------------------
/custom_components/airco2ntrol/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "airco2ntrol",
3 | "name": "airco2ntrol CO2 Sensor",
4 | "codeowners": ["@leorbs"],
5 | "config_flow": true,
6 | "dependencies": [],
7 | "documentation": "https://github.com/leorbs/airco2ntrol",
8 | "iot_class": "local_polling",
9 | "issue_tracker": "https://github.com/leorbs/airco2ntrol/issues",
10 | "requirements": [],
11 | "version": "0.5.0"
12 | }
13 |
--------------------------------------------------------------------------------
/custom_components/airco2ntrol/config_flow.py:
--------------------------------------------------------------------------------
1 | """Config flow for AirCO2ntrol integration."""
2 | import logging
3 | import voluptuous as vol
4 | from homeassistant import config_entries
5 |
6 | from . import DOMAIN
7 |
8 | _LOGGER = logging.getLogger(__name__)
9 |
10 | class AirCO2ntrolConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
11 | """Handle a config flow for AirCO2ntrol."""
12 |
13 | VERSION = 1
14 |
15 | async def async_step_user(self, user_input=None):
16 | """Handle the initial step."""
17 | errors = {}
18 | if user_input is not None:
19 | # Create the config entry
20 | return self.async_create_entry(title="AirCO2ntrol", data={})
21 |
22 | return self.async_show_form(
23 | step_id="user",
24 | data_schema=vol.Schema({}),
25 | errors=errors,
26 | )
27 |
--------------------------------------------------------------------------------
/custom_components/airco2ntrol/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from homeassistant.core import HomeAssistant
3 | from homeassistant.config_entries import ConfigEntry
4 | from homeassistant.helpers.discovery import async_load_platform
5 |
6 | DOMAIN = "airco2ntrol"
7 | PLATFORMS = ["sensor"]
8 |
9 | _LOGGER = logging.getLogger(__name__)
10 |
11 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
12 | """Set up AirCO2ntrol from a config entry."""
13 | _LOGGER.info("Setting up AirCO2ntrol integration")
14 |
15 | hass.data.setdefault(DOMAIN, {})
16 |
17 | # Load sensor platform
18 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
19 |
20 | return True
21 |
22 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
23 | """Unload an AirCO2ntrol config entry."""
24 | return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 leorbs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/logos/logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/logos/tfa-logo-2021.svg:
--------------------------------------------------------------------------------
1 |
2 |
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Aircontrol CO2 Monitor Mini TFA
2 |
3 | A custom [home-assistant](https://www.home-assistant.io/) component for a family of compatible CO2 monitors distributed under various names
4 | - [TFA Dostmann CO2 Monitor AIRCO2NTROL MINI](https://www.tfa-dostmann.de/en/produkt/co2-monitor-airco2ntrol-mini/).
5 | - [ZyAura ZGm053U](https://www.zyaura.com/product-detail/zgm053u/)
6 | - [TFA Dostmann CO2 Monitor AIRCO2NTROL COACH](https://www.tfa-dostmann.de/en/product/co2-monitor-airco2ntrol-coach-31-5009/)
7 | - [ZyAura ZGm27](https://www.zyaura.com/product-detail/zgm27/)
8 |
9 | Idea based on "[Reverse-Engineering a low-cost USB CO₂ monitor](https://hackaday.io/project/5301-reverse-engineering-a-low-cost-usb-co-monitor)".
10 | Thx [Henryk Plötz](https://hackaday.io/henryk). Code for this HA integration originally taken from [jansauer](https://github.com/jansauer/home-assistant_config/tree/master/config/custom_components/airco2ntrol).
11 | Some older devices have a static encryption (as [Henryk Plötz](https://hackaday.io/henryk) found out).
12 | This integration is both, with the newer and older devices compatible.
13 |
14 | ## Setup
15 | If you a very old version from this repository installed, remove ``airco2ntrol`` from your `configuration.yaml`
16 | ### Automatic
17 | 1. Visit your HACS and install the airco2ntrol integration from there
18 | 2. Follow step 2. from the "Manual" Section below
19 |
20 | ### Manual
21 | 1. Upload the `custom_components/airco2ntrol` folder to your `custom_components` folder (using Samba or FTP addons).
22 | It should look like `config/custom_components/airco2ntrol/`.
23 | 2. Restart your Home Assistant.
24 | 3. Go to your Integrations, click on "ADD INTEGRATION" and search for "`co2`"
25 | 4. choose ``airco2ntrol`` and click on "SUBMIT"
26 | 5. The integration should create 3 entities as soon as you have the CO2 sensor attached to your server
27 |
28 | ## Functionality
29 | This is how your sensors measurement might look:
30 |
31 | 
32 |
33 | This integration should provide:
34 | - CO2 values
35 | - Temperature
36 | - OPTIONAL: Humidity
37 |
38 | Some devices appear to have a humidity readings. If they do so, then the humidity entity will be usable.
39 |
40 | ## Notes
41 | The pdf in this repository describes the usb protocol used by airco2ntrol device
42 |
43 |
--------------------------------------------------------------------------------
/custom_components/airco2ntrol/sensor_reader.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | IDX_FNK = 0
4 | IDX_MSB = 1
5 | IDX_LSB = 2
6 | IDX_CHK = 3
7 |
8 | KEY = [0xc4, 0xc6, 0xc0, 0x92, 0x40, 0x23, 0xdc, 0x96]
9 | CSTATE = [0x48, 0x74, 0x65, 0x6D, 0x70, 0x39, 0x39, 0x65]
10 | SHUFFLE = [2, 4, 0, 7, 1, 6, 5, 3]
11 |
12 | POLL_MODE_NORMAL = 'normal'
13 | POLL_MODE_DECRYPT = 'decrypt'
14 |
15 | _LOGGER = logging.getLogger(__name__)
16 |
17 | def _decrypt(data):
18 | phase1 = [0] * 8
19 | for i, o in enumerate(SHUFFLE):
20 | phase1[o] = data[i]
21 | phase2 = [0] * 8
22 | for i in range(8):
23 | phase2[i] = phase1[i] ^ KEY[i]
24 | phase3 = [0] * 8
25 | for i in range(8):
26 | phase3[i] = ( (phase2[i] >> 3) | (phase2[ (i-1+8)%8 ] << 5) ) & 0xff
27 | ctmp = [0] * 8
28 | for i in range(8):
29 | ctmp[i] = ( (CSTATE[i] >> 4) | (CSTATE[i]<<4) ) & 0xff
30 | out = [0] * 8
31 | for i in range(8):
32 | out[i] = (0x100 + phase3[i] - ctmp[i]) & 0xff
33 | return out
34 |
35 | class SensorReader:
36 | def __init__(self, fp):
37 | self._fp = fp
38 | self._mode = None
39 |
40 | def _init(self):
41 | _LOGGER.info("Trying to poll in 'normal' mode")
42 | data = self._poll_normal()
43 | if data is not None:
44 | _LOGGER.info("Setting poll mode to 'normal' mode")
45 | self._mode = POLL_MODE_NORMAL
46 | return
47 |
48 | _LOGGER.info("Trying to poll in 'decrypt' mode")
49 | data = self._poll_decrypt()
50 | if data is not None:
51 | _LOGGER.info("Setting poll mode to 'decrypt' mode")
52 | self._mode = POLL_MODE_DECRYPT
53 | return
54 |
55 | _LOGGER.warning("neither 'normal', nor 'decrypt' mode was successful")
56 |
57 |
58 | def poll_function_and_value(self):
59 | data = self._poll()
60 | if data is None:
61 | return None
62 | calculated_value = (data[IDX_MSB] << 8) | data[IDX_LSB]
63 | return data[IDX_FNK], calculated_value
64 |
65 | def _poll(self):
66 | if self._mode is None:
67 | self._init()
68 |
69 | if self._mode == POLL_MODE_NORMAL:
70 | return self._poll_normal()
71 | elif self._mode == POLL_MODE_DECRYPT:
72 | return self._poll_decrypt()
73 |
74 | def _poll_normal(self):
75 | data = list(self._fp.read(5))
76 | if ((data[IDX_MSB] + data[IDX_LSB] + data[IDX_FNK]) % 256) != data[IDX_CHK]:
77 | _LOGGER.info("Checksum incorrect for 'normal' mode: %s", data)
78 | return None
79 | else:
80 | return data
81 |
82 | def _poll_decrypt(self):
83 | data = list(self._fp.read(8))
84 | decrypted = _decrypt(data)
85 | if decrypted[4] != 0x0d or (sum(decrypted[:3]) & 0xff) != decrypted[3]:
86 | _LOGGER.info("Checksum incorrect for 'decrypt' mode: %s", data)
87 | return None
88 | else:
89 | return decrypted
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/custom_components/airco2ntrol/sensor.py:
--------------------------------------------------------------------------------
1 | """
2 | Home Assistant support for the TFA Dostmann: CO2 Monitor AIRCO2NTROL MINI sensor.
3 |
4 | Original Implementation:
5 | Homepage: https://github.com/jansauer/home-assistant_config/tree/master/config/custom_components/airco2ntrol
6 | """
7 | import fcntl
8 | import logging
9 | import os
10 | import datetime
11 |
12 | from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorStateClass
13 | from homeassistant.const import UnitOfTemperature, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE
14 | from homeassistant.exceptions import ConfigEntryNotReady
15 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, CoordinatorEntity
16 | from homeassistant.helpers.entity_platform import AddEntitiesCallback
17 | from homeassistant.core import HomeAssistant
18 | from homeassistant.config_entries import ConfigEntry
19 | from custom_components.airco2ntrol.sensor_reader import SensorReader
20 |
21 | _LOGGER = logging.getLogger(__name__)
22 |
23 | HIDIOCSFEATURE_9 = 0xC0094806
24 |
25 |
26 | POLL_INTERVAL = 20 # seconds
27 | POLL_INTERVAL_TIMEDELTA = datetime.timedelta(seconds=POLL_INTERVAL)
28 |
29 |
30 | CONVERGENCE_SPEED = 20
31 |
32 | HID_KEYWORDS = ["Holtek", "zyTemp"] # Adjust based on your actual device name
33 |
34 |
35 | def get_device_unique_id(file):
36 | """Get a unique ID for the HID device based on available attributes."""
37 | try:
38 | hid_uniq = None
39 | hid_id = None
40 |
41 | for line in file:
42 | if line.startswith("HID_UNIQ="):
43 | hid_uniq = line.strip().split("=")[1]
44 | elif line.startswith("HID_ID="):
45 | parts = line.strip().split("=")[1].split(":")
46 | if len(parts) >= 3:
47 | hid_id = f"{parts[1]}:{parts[2]}" # Extract VID:PID
48 |
49 | # Prefer HID_UNIQ if available
50 | if hid_uniq and hid_uniq != "":
51 | return hid_uniq
52 | elif hid_id:
53 | return hid_id
54 |
55 | except FileNotFoundError:
56 | _LOGGER.warning(f"Cannot read {file}. Cannot determine unique ID of device")
57 | raise
58 |
59 |
60 | def get_device_path():
61 | """Find the correct HID device and return (device_path, unique_id)."""
62 | try:
63 | for device in os.listdir('/sys/class/hidraw/'):
64 | uevent_path = f"/sys/class/hidraw/{device}/device/uevent"
65 |
66 | try:
67 | with open(uevent_path, "r") as file:
68 | for line in file:
69 | if line.startswith("HID_NAME="):
70 | device_name = line.strip().split("=")[1]
71 | if any(keyword in device_name for keyword in HID_KEYWORDS):
72 | unique_id = get_device_unique_id(file)
73 | return f"/dev/{device}", unique_id
74 |
75 | except FileNotFoundError:
76 | _LOGGER.warning(f"Cannot read {uevent_path}, skipping.")
77 |
78 | raise FileNotFoundError("No matching HID device found.")
79 |
80 | except Exception as e:
81 | _LOGGER.error(f"Error finding HID device: {e}")
82 | raise
83 |
84 | class AirCO2ntrolReader:
85 | """Class to interact with the AirCO2ntrol sensor."""
86 |
87 | def __init__(self):
88 | """Initialize the reader."""
89 | self.carbon_dioxide = None
90 | self.temperature = None
91 | self.humidity = None
92 | self._sensorReader = None
93 |
94 | def _recover(self):
95 | """Attempt to recover the connection to the device."""
96 | try:
97 | self.device_path, _ = get_device_path()
98 | _LOGGER.info("Trying to initialize connection...")
99 | _fp = open(self.device_path, 'ab+', 0)
100 | _LOGGER.info("Setting connection mode...")
101 | fcntl.ioctl(_fp, HIDIOCSFEATURE_9, bytearray.fromhex('00 c4 c6 c0 92 40 23 dc 96'))
102 | self._sensorReader = SensorReader(_fp)
103 | except FileNotFoundError as e:
104 | _LOGGER.warning(f"Did not find HID device. Is it plugged in? Message: {e}")
105 | self._sensorReader = None
106 | except Exception as e:
107 | _LOGGER.error(f"Device initialization failed: {e}")
108 | self._sensorReader = None
109 |
110 | def update(self):
111 | """Poll the latest sensor data."""
112 | if not self._sensorReader:
113 | _LOGGER.info("Currently no device connected. Trying to find and connect to CO2 Device.")
114 | self._recover()
115 | if not self._sensorReader:
116 | return {
117 | "co2": None,
118 | "temperature": None,
119 | "humidity": None,
120 | "available": False
121 | }
122 |
123 | _LOGGER.debug("Polling latest sensor data.")
124 | got_carbon_dioxide = None
125 | got_temperature = None
126 | got_humidity = None
127 | for _ in range(CONVERGENCE_SPEED): # Try a few times
128 | function, value = self._safe_poll_function_and_value()
129 | if not function or not value:
130 | continue
131 | if function == 0x50:
132 | if value > 10000:
133 | # sometimes the first read of this value is something around 25k.
134 | # This is a safety to filter such implausible readings
135 | continue
136 | self.carbon_dioxide = value
137 | got_carbon_dioxide = True
138 | elif function == 0x42:
139 | self.temperature = value / 16.0 - 273.15
140 | got_temperature = True
141 | elif function == 0x41:
142 | self.humidity = value / 100
143 | got_humidity = True
144 |
145 | _LOGGER.debug(f"Got new values for carbon_dioxide:{got_carbon_dioxide} temperature:{got_temperature} humidity:{got_humidity}")
146 | return {
147 | # return all values, even if they are not the most recent
148 | "co2": self.carbon_dioxide,
149 | "temperature": self.temperature,
150 | "humidity": self.humidity,
151 | "available": True
152 | }
153 |
154 | def _safe_poll_function_and_value(self):
155 | try:
156 | return self._sensorReader.poll_function_and_value()
157 | except Exception as e:
158 | _LOGGER.warning(f"Error reading sensor data. Resetting device connection: {e}")
159 | self._sensorReader = None
160 | return None, None
161 |
162 |
163 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
164 | """Set up sensors from a config entry using the new DataUpdateCoordinator."""
165 |
166 | try:
167 | _, unique_id = await hass.async_add_executor_job(get_device_path)
168 | except Exception as e:
169 | raise ConfigEntryNotReady(f"Could not setup device yet: {e}")
170 |
171 | reader = AirCO2ntrolReader()
172 |
173 | async def async_update():
174 | _LOGGER.info("async_update called")
175 | data = await hass.async_add_executor_job(reader.update)
176 | _LOGGER.debug(f"Fetched sensor data: {data}")
177 | return data
178 |
179 | coordinator = DataUpdateCoordinator(
180 | hass=hass,
181 | logger=_LOGGER,
182 | name="AirCO2ntrol",
183 | update_method=async_update,
184 | update_interval=POLL_INTERVAL_TIMEDELTA
185 | )
186 |
187 | await coordinator.async_config_entry_first_refresh()
188 |
189 | async_add_entities([
190 | AirCO2ntrolCarbonDioxideSensor(coordinator, unique_id),
191 | AirCO2ntrolTemperatureSensor(coordinator, unique_id),
192 | AirCO2ntrolHumiditySensor(coordinator, unique_id)
193 | ])
194 |
195 |
196 | class AirCO2ntrolSensor(CoordinatorEntity, SensorEntity):
197 | """Base class for AirCO2ntrol sensors."""
198 |
199 | def __init__(self, coordinator, name, sensor_type, unit, icon, device_class, unique_id):
200 | """Initialize the sensor."""
201 | super().__init__(coordinator)
202 | self._attr_name = name
203 | self._attr_native_unit_of_measurement = unit
204 | self._attr_icon = icon
205 | self._attr_device_class = device_class
206 | self._attr_unique_id = f"{unique_id}-{sensor_type}"
207 | self._attr_state_class = SensorStateClass.MEASUREMENT
208 | self.sensor_type = sensor_type
209 |
210 | @property
211 | def native_value(self):
212 | """Return the current sensor state."""
213 | value = self.coordinator.data.get(self.sensor_type)
214 | _LOGGER.debug(f"Sensor {self._attr_unique_id} updated: {value}")
215 | return value
216 |
217 |
218 | @property
219 | def available(self) -> bool:
220 | return self.coordinator.data.get("available")
221 |
222 |
223 | class AirCO2ntrolCarbonDioxideSensor(AirCO2ntrolSensor):
224 | """CO2 Sensor."""
225 |
226 | def __init__(self, coordinator, unique_id):
227 | super().__init__(
228 | coordinator,
229 | name="AirCO2ntrol Carbon Dioxide",
230 | sensor_type="co2",
231 | unit=CONCENTRATION_PARTS_PER_MILLION,
232 | icon="mdi:molecule-co2",
233 | device_class=SensorDeviceClass.CO2,
234 | unique_id=unique_id
235 | )
236 |
237 |
238 | class AirCO2ntrolTemperatureSensor(AirCO2ntrolSensor):
239 | """Temperature Sensor."""
240 |
241 | def __init__(self, coordinator, unique_id):
242 | super().__init__(
243 | coordinator,
244 | name="AirCO2ntrol Temperature",
245 | sensor_type="temperature",
246 | unit=UnitOfTemperature.CELSIUS,
247 | icon="mdi:thermometer",
248 | device_class=SensorDeviceClass.TEMPERATURE,
249 | unique_id=unique_id
250 | )
251 |
252 |
253 | class AirCO2ntrolHumiditySensor(AirCO2ntrolSensor):
254 | """Humidity Sensor."""
255 |
256 | def __init__(self, coordinator, unique_id):
257 | super().__init__(
258 | coordinator,
259 | name="AirCO2ntrol Humidity",
260 | sensor_type="humidity",
261 | unit=PERCENTAGE,
262 | icon="mdi:water-percent",
263 | device_class=SensorDeviceClass.HUMIDITY,
264 | unique_id=unique_id
265 | )
266 |
--------------------------------------------------------------------------------