Add a carbon dioxide sensor to homematicip_cloud (#179187)

This commit is contained in:
Christian Lackas
2026-08-15 09:25:40 +02:00
committed by GitHub
parent c24d21ee81
commit 400a2cad4b
3 changed files with 460 additions and 0 deletions
@@ -146,6 +146,12 @@ ATTR_TEMPERATURE_OFFSET = "temperature_offset"
ATTR_WIND_DIRECTION = "wind_direction"
ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree"
ATTR_ESI_TYPE = "type"
CARBON_DIOXIDE_CHANNELS = (
FunctionalChannelType.CARBON_DIOXIDE_SENSOR_CHANNEL,
FunctionalChannelType.WALL_MOUNTED_THERMOSTAT_WITH_CARBON_CHANNEL,
)
ESI_TYPE_UNKNOWN = "UNKNOWN"
ESI_CONNECTED_SENSOR_TYPE_IEC = "ES_IEC"
ESI_CONNECTED_SENSOR_TYPE_GAS = "ES_GAS"
@@ -368,6 +374,14 @@ async def async_setup_entry(
if smoke_detector_channel_data_exists(device, description.channel_field)
)
entities.extend(
HomematicipCarbonDioxideSensor(hap, device, channel.index)
for device in hap.home.devices
for channel_type in CARBON_DIOXIDE_CHANNELS
for channel in get_channels_from_device(device, channel_type)
if getattr(channel, "carbonDioxideConcentration", None) is not None
)
async_add_entities(entities)
@@ -620,6 +634,33 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity):
return round(self._device.valvePosition * 100)
class HomematicipCarbonDioxideSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP carbon dioxide sensor."""
_attr_device_class = SensorDeviceClass.CO2
_attr_native_unit_of_measurement = UnitOfRatio.PARTS_PER_MILLION
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, hap: HomematicipHAP, device: Device, channel: int) -> None:
"""Initialize the carbon dioxide sensor."""
super().__init__(
hap,
device,
channel=channel,
# the channel is not at the same index on every device
channel_real_index=channel,
is_multi_channel=True,
feature_id="carbon_dioxide",
use_description_name=True,
)
@property
@override
def native_value(self) -> float | None:
"""Return the state."""
return self.get_channel_or_raise().carbonDioxideConcentration
class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP humidity sensor."""
@@ -417,3 +417,366 @@ def mock_connection_init_fixture():
def simple_mock_auth_fixture() -> Auth:
"""Return a simple AsyncAuth Mock."""
return AsyncMock(spec=Auth, pin=HAPPIN, create=True)
@pytest.fixture(name="wall_mounted_thermostat_with_carbon_device_data")
def wall_mounted_thermostat_with_carbon_device_data_fixture() -> dict[str, Any]:
"""Return fixture data for an HmIP-WGTC device."""
return {
"availableFirmwareVersion": "1.0.18",
"connectionType": "HMIP_RF",
"deviceArchetype": "HMIP",
"firmwareVersion": "1.0.18",
"firmwareVersionInteger": 65554,
"functionalChannels": {
"0": {
"altitude": None,
"bootedRecently": False,
"busConfigMismatch": None,
"coProFaulty": False,
"coProRestartNeeded": False,
"coProUpdateFailure": False,
"configPending": False,
"controlsMountingOrientation": None,
"daliBusState": None,
"dataDecodingFailedError": None,
"defaultLinkedGroup": [],
"deviceAliveSignalEnabled": None,
"deviceCanBusError": None,
"deviceCommunicationError": None,
"deviceDriveError": None,
"deviceDriveModeError": None,
"deviceId": "3014F711000000000000WGTC",
"deviceOperationMode": None,
"deviceOverheated": False,
"deviceOverloaded": False,
"devicePowerFailureDetected": False,
"deviceUndervoltage": False,
"displayContrast": None,
"displayMode": None,
"displayMountingOrientation": None,
"dutyCycle": False,
"fanControlMode": None,
"frostProtectionError": None,
"frostProtectionErrorAcknowledged": None,
"functionalChannelType": "DEVICE_OPERATIONLOCK",
"groupIndex": 0,
"groups": [],
"index": 0,
"input1CoproEnabled": None,
"input2CoproEnabled": None,
"input3CoproEnabled": None,
"input4CoproEnabled": None,
"inputLayoutMode": None,
"invertedDisplayColors": None,
"label": "",
"lastBootTimestamp": 1784278839458,
"lockJammed": None,
"lowBat": None,
"mountingModuleError": None,
"mountingOrientation": None,
"multicastRoutingEnabled": False,
"noDataFromLinkyError": None,
"notRechargeableBattery": None,
"operationDays": None,
"operationLockActive": False,
"particulateMatterSensorCommunicationError": None,
"particulateMatterSensorError": None,
"powerShortCircuit": None,
"profilePeriodLimitReached": None,
"routerModuleEnabled": False,
"routerModuleSupported": False,
"rssiDeviceValue": -75,
"rssiPeerValue": None,
"sensorCommunicationError": None,
"sensorError": None,
"shortCircuitDataLine": None,
"supportedOptionalFeatures": {
"IFeatureDeviceIdentify": True,
"IFeatureDeviceMountingModuleError": True,
"IFeatureDeviceOverloaded": True,
"IFeatureProfilePeriodLimit": True,
"IFeatureRssiValue": True,
"IOptionalFeatureDeviceSwitchChannelMode": True,
"IOptionalFeatureDutyCycle": True,
},
"switchChannelMode": "FLOOR_HEATING",
"temperatureHumiditySensorCommunicationError": None,
"temperatureHumiditySensorError": None,
"temperatureOutOfRange": False,
"ticVersionError": None,
"unreach": False,
"valveFlowError": None,
"valveWaterError": None,
},
"1": {
"channelRole": "KEY_OR_SWITCH_FOR_GROUP",
"deviceId": "3014F711000000000000WGTC",
"functionalChannelType": "INPUT_QUICK_ACTION_DISPLAY_CHANNEL",
"groupIndex": 1,
"groups": [],
"index": 1,
"label": "",
"supportedOptionalFeatures": {
"IFeatureLightGroupSensorChannel": True,
"IFeatureShadingGroupSensorChannel": True,
},
},
"2": {
"channelRole": "DIMMING_ACTUATOR",
"deviceId": "3014F711000000000000WGTC",
"dimLevel": 0.02,
"functionalChannelType": "BACKLIGHT_CHANNEL",
"groupIndex": 2,
"groups": [],
"index": 2,
"label": "",
"on": True,
"powerUpDimLevel": 0.02,
"powerUpSwitchState": "PERMANENT_ON",
"profileMode": "AUTOMATIC",
"supportedOptionalFeatures": {
"IFeatureLightProfileActuatorChannel": True
},
"switchVisualization": "LIGHT",
"userDesiredProfileMode": "AUTOMATIC",
},
"3": {
"actualTemperature": 22.6,
"carbonDioxideConcentration": 440.0,
"channelRole": "WALL_MOUNTED_THERMOSTAT",
"deviceId": "3014F711000000000000WGTC",
"display": "ACTUAL",
"functionalChannelType": "WALL_MOUNTED_THERMOSTAT_WITH_CARBON_CHANNEL",
"groupIndex": 3,
"groups": [],
"humidity": 48,
"index": 3,
"label": "Wandthermostat (3 + 4) Child",
"setPointTemperature": 18.0,
"supportedOptionalFeatures": {
"IOptionalFeatureClimateControlDisplayCarbonSupported": True,
"IOptionalFeatureClimateControlDisplayHumidityOnlySupported": True,
"IOptionalFeatureThermostatCoolingSupported": True,
},
"temperatureOffset": -0.5,
"vaporAmount": 9.619851889906071,
},
"4": {
"climateControlType": "PWM_CONTROL",
"deviceId": "3014F711000000000000WGTC",
"frostProtectionTemperature": 8.0,
"functionalChannelType": "INTERNAL_SWITCH_CHANNEL",
"groupIndex": 3,
"groups": [],
"heatingValveType": "NORMALLY_CLOSE",
"index": 4,
"internalSwitchOutputEnabled": False,
"label": "Wandthermostat (3 + 4) Child",
"supportedOptionalFeatures": {
"IOptionalFeatureClimateControlType": True,
"IOptionalFeatureFloorHeatingSpecificGroupSupported": True,
},
"valvePosition": 0.0,
"valveProtectionDuration": 5,
"valveProtectionSwitchingInterval": 14,
},
"5": {
"channelRole": None,
"deviceId": "3014F711000000000000WGTC",
"functionalChannelType": "SWITCH_CHANNEL",
"groupIndex": 0,
"groups": [],
"index": 5,
"internalLinkConfiguration": None,
"label": "",
"on": False,
"powerUpSwitchState": "PERMANENT_OFF",
"profileMode": "AUTOMATIC",
"supportedOptionalFeatures": {
"IOptionalFeaturePowerUpSwitchState": True
},
"switchVisualization": None,
"userDesiredProfileMode": "AUTOMATIC",
},
"6": {
"deviceId": "3014F711000000000000WGTC",
"functionalChannelType": "HEAT_DEMAND_CHANNEL",
"groupIndex": 0,
"groups": [],
"index": 6,
"label": "",
},
"7": {
"deviceId": "3014F711000000000000WGTC",
"functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL",
"groupIndex": 0,
"groups": [],
"index": 7,
"label": "",
},
"8": {
"deviceId": "3014F711000000000000WGTC",
"functionalChannelType": "CHANGE_OVER_CHANNEL",
"groupIndex": 0,
"groups": [],
"index": 8,
"label": "",
},
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F711000000000000WGTC",
"label": "Wandthermostat mit CO2",
"lastStatusUpdate": 1784553668564,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manuallyUpdateForced": False,
"manufacturerCode": 1,
"measuredAttributes": {
"3": {
"actualTemperature": True,
"carbonDioxideConcentration": True,
"humidity": True,
"setPointTemperature": True,
}
},
"modelId": 568,
"modelType": "HmIP-WGTC",
"oem": "eQ-3",
"permanentlyReachable": True,
"serializedGlobalTradeItemNumber": "3014F711000000000000WGTC",
"type": "WALL_MOUNTED_GLASS_THERMOSTAT_CARBON",
"updateState": "UP_TO_DATE",
}
@pytest.fixture(name="carbon_dioxide_sensor_device_data")
def carbon_dioxide_sensor_device_data_fixture() -> dict[str, Any]:
"""Return fixture data for an HmIP-SCTH230 device."""
return {
"availableFirmwareVersion": "1.0.6",
"connectionType": "HMIP_RF",
"deviceArchetype": "HMIP",
"firmwareVersion": "1.0.6",
"firmwareVersionInteger": 65542,
"functionalChannels": {
"0": {
"busConfigMismatch": None,
"coProFaulty": False,
"coProRestartNeeded": False,
"coProUpdateFailure": False,
"configPending": False,
"controlsMountingOrientation": None,
"deviceCommunicationError": None,
"deviceDriveError": None,
"deviceDriveModeError": None,
"deviceId": "3014F711000000000SCTH230",
"deviceOperationMode": None,
"deviceOverheated": False,
"deviceOverloaded": False,
"devicePowerFailureDetected": False,
"deviceUndervoltage": False,
"displayContrast": None,
"dutyCycle": False,
"functionalChannelType": "DEVICE_BASE",
"groupIndex": 0,
"groups": ["00000000-0000-0000-0000-000000000052"],
"index": 0,
"label": "",
"lockJammed": None,
"lowBat": None,
"mountingOrientation": None,
"multicastRoutingEnabled": False,
"particulateMatterSensorCommunicationError": None,
"particulateMatterSensorError": None,
"powerShortCircuit": None,
"profilePeriodLimitReached": None,
"routerModuleEnabled": False,
"routerModuleSupported": False,
"rssiDeviceValue": -66,
"rssiPeerValue": None,
"shortCircuitDataLine": None,
"supportedOptionalFeatures": {
"IFeatureBusConfigMismatch": False,
"IFeatureDeviceCoProError": False,
"IFeatureDeviceCoProRestart": False,
"IFeatureDeviceCoProUpdate": False,
"IFeatureDeviceCommunicationError": False,
"IFeatureDeviceDriveError": False,
"IFeatureDeviceDriveModeError": False,
"IFeatureDeviceIdentify": False,
"IFeatureDeviceOverheated": False,
"IFeatureDeviceOverloaded": False,
"IFeatureDeviceParticulateMatterSensorCommunicationError": False,
"IFeatureDeviceParticulateMatterSensorError": False,
"IFeatureDevicePowerFailure": False,
"IFeatureDeviceTemperatureHumiditySensorCommunicationError": False,
"IFeatureDeviceTemperatureHumiditySensorError": False,
"IFeatureDeviceTemperatureOutOfRange": False,
"IFeatureDeviceUndervoltage": False,
"IFeatureMulticastRouter": False,
"IFeaturePowerShortCircuit": False,
"IFeatureProfilePeriodLimit": True,
"IFeatureRssiValue": True,
"IFeatureShortCircuitDataLine": False,
"IOptionalFeatureDeviceErrorLockJammed": False,
"IOptionalFeatureDeviceOperationMode": False,
"IOptionalFeatureDisplayContrast": False,
"IOptionalFeatureDutyCycle": True,
"IOptionalFeatureLowBat": False,
"IOptionalFeatureMountingOrientation": False,
},
"temperatureHumiditySensorCommunicationError": None,
"temperatureHumiditySensorError": None,
"temperatureOutOfRange": False,
"unreach": False,
},
"1": {
"actualTemperature": 25.5,
"carbonDioxideConcentration": 1181.0,
"carbonDioxideVisualisationEnabled": True,
"channelRole": "CARBON_DIOXIDE_SENSOR",
"deviceId": "3014F711000000000SCTH230",
"functionalChannelType": "CARBON_DIOXIDE_SENSOR_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000053"],
"humidity": 37,
"index": 1,
"label": "",
"vaporAmount": 8.739326558877478,
},
"2": {
"channelRole": "SWITCH_ACTUATOR",
"deviceId": "3014F711000000000SCTH230",
"functionalChannelType": "SWITCH_CHANNEL",
"groupIndex": 2,
"groups": ["00000000-0000-0000-0000-000000000054"],
"index": 2,
"internalLinkConfiguration": None,
"label": "",
"on": False,
"powerUpSwitchState": "PERMANENT_OFF",
"profileMode": "AUTOMATIC",
"supportedOptionalFeatures": {
"IFeatureAccessAuthorizationActuatorChannel": False,
"IFeatureLightGroupActuatorChannel": True,
"IFeatureLightProfileActuatorChannel": True,
"IOptionalFeatureInternalLinkConfiguration": False,
"IOptionalFeaturePowerUpSwitchState": True,
},
"userDesiredProfileMode": "AUTOMATIC",
},
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F711000000000SCTH230",
"label": "CO2 Sensor miko ",
"lastStatusUpdate": 1676606756797,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
"modelId": 435,
"modelType": "HmIP-SCTH230",
"oem": "eQ-3",
"permanentlyReachable": True,
"serializedGlobalTradeItemNumber": "3014F711000000000SCTH230",
"type": "CARBON_DIOXIDE_SENSOR",
"updateState": "UP_TO_DATE",
}
@@ -35,6 +35,7 @@ from homeassistant.const import (
STATE_UNKNOWN,
UnitOfLength,
UnitOfPower,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolume,
@@ -1079,3 +1080,58 @@ async def test_hmip_soil_temperature_sensor(
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "18.3"
@pytest.mark.parametrize(
("device_data_fixture", "device_label", "entity_id", "device_model", "channel"),
[
(
"wall_mounted_thermostat_with_carbon_device_data",
"Wandthermostat mit CO2",
"sensor.wandthermostat_mit_co2_carbon_dioxide",
"HmIP-WGTC",
3,
),
(
"carbon_dioxide_sensor_device_data",
"CO2 Sensor miko ",
"sensor.co2_sensor_miko_carbon_dioxide",
"HmIP-SCTH230",
1,
),
],
)
async def test_hmip_carbon_dioxide_sensor(
hass: HomeAssistant,
default_mock_hap_factory: HomematicipHAP,
request: pytest.FixtureRequest,
device_data_fixture: str,
device_label: str,
entity_id: str,
device_model: str,
channel: int,
) -> None:
"""Test the carbon dioxide sensor on both channel types."""
device_data = request.getfixturevalue(device_data_fixture)
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=[device_label], extra_devices=[device_data]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, f"{device_label} Carbon dioxide", device_model
)
assert (
ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfRatio.PARTS_PER_MILLION
)
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
await async_manipulate_test_data(
hass,
hmip_device,
"carbonDioxideConcentration",
812.0,
channel=channel,
channel_real_index=channel,
)
assert hass.states.get(entity_id).state == "812.0"