Add device class gas and enable statistics for it (#54110)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Bram Kragten 2021-08-11 18:58:19 +02:00 committed by GitHub
parent 94a264afaf
commit e23750b2a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 146 additions and 39 deletions

View File

@ -9,6 +9,7 @@ from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
) )
@ -256,6 +257,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
is_gas=True, is_gas=True,
force_update=True, force_update=True,
icon="mdi:fire", icon="mdi:fire",
device_class=DEVICE_CLASS_GAS,
last_reset=dt.utc_from_timestamp(0), last_reset=dt.utc_from_timestamp(0),
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
@ -266,6 +268,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
is_gas=True, is_gas=True,
force_update=True, force_update=True,
icon="mdi:fire", icon="mdi:fire",
device_class=DEVICE_CLASS_GAS,
last_reset=dt.utc_from_timestamp(0), last_reset=dt.utc_from_timestamp(0),
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
@ -276,6 +279,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
is_gas=True, is_gas=True,
force_update=True, force_update=True,
icon="mdi:fire", icon="mdi:fire",
device_class=DEVICE_CLASS_GAS,
last_reset=dt.utc_from_timestamp(0), last_reset=dt.utc_from_timestamp(0),
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),

View File

@ -16,7 +16,12 @@ import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.const import (
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
VOLUME_CUBIC_METERS,
)
from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.core import CoreState, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -56,6 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
} }
) )
UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS}
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -260,7 +267,10 @@ class DSMREntity(SensorEntity):
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity, if any.""" """Return the unit of measurement of this entity, if any."""
return self.get_dsmr_object_attr("unit") unit_of_measurement = self.get_dsmr_object_attr("unit")
if unit_of_measurement in UNIT_CONVERSION:
return UNIT_CONVERSION[unit_of_measurement]
return unit_of_measurement
@staticmethod @staticmethod
def translate_tariff(value: str, dsmr_version: str) -> str | None: def translate_tariff(value: str, dsmr_version: str) -> str | None:

View File

@ -11,13 +11,19 @@ from sqlalchemy import bindparam
from sqlalchemy.ext import baked from sqlalchemy.ext import baked
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from homeassistant.const import PRESSURE_PA, TEMP_CELSIUS from homeassistant.const import (
PRESSURE_PA,
TEMP_CELSIUS,
VOLUME_CUBIC_FEET,
VOLUME_CUBIC_METERS,
)
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import homeassistant.util.pressure as pressure_util import homeassistant.util.pressure as pressure_util
import homeassistant.util.temperature as temperature_util import homeassistant.util.temperature as temperature_util
from homeassistant.util.unit_system import UnitSystem from homeassistant.util.unit_system import UnitSystem
import homeassistant.util.volume as volume_util
from .const import DOMAIN from .const import DOMAIN
from .models import ( from .models import (
@ -64,6 +70,11 @@ UNIT_CONVERSIONS = {
) )
if x is not None if x is not None
else None, else None,
VOLUME_CUBIC_METERS: lambda x, units: volume_util.convert(
x, VOLUME_CUBIC_METERS, _configured_unit(VOLUME_CUBIC_METERS, units)
)
if x is not None
else None,
} }
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -214,6 +225,10 @@ def _configured_unit(unit: str, units: UnitSystem) -> str:
return units.pressure_unit return units.pressure_unit
if unit == TEMP_CELSIUS: if unit == TEMP_CELSIUS:
return units.temperature_unit return units.temperature_unit
if unit == VOLUME_CUBIC_METERS:
if units.is_metric:
return VOLUME_CUBIC_METERS
return VOLUME_CUBIC_FEET
return unit return unit

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
DEVICE_CLASS_CO2, DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_MONETARY, DEVICE_CLASS_MONETARY,
@ -65,6 +66,7 @@ DEVICE_CLASSES: Final[list[str]] = [
DEVICE_CLASS_POWER, # power (W/kW) DEVICE_CLASS_POWER, # power (W/kW)
DEVICE_CLASS_POWER_FACTOR, # power factor (%) DEVICE_CLASS_POWER_FACTOR, # power factor (%)
DEVICE_CLASS_VOLTAGE, # voltage (V) DEVICE_CLASS_VOLTAGE, # voltage (V)
DEVICE_CLASS_GAS, # gas (m³ or ft³)
] ]
DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))

View File

@ -16,6 +16,7 @@ from homeassistant.const import (
DEVICE_CLASS_CO2, DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
@ -46,6 +47,7 @@ CONF_IS_CO2 = "is_carbon_dioxide"
CONF_IS_CURRENT = "is_current" CONF_IS_CURRENT = "is_current"
CONF_IS_ENERGY = "is_energy" CONF_IS_ENERGY = "is_energy"
CONF_IS_HUMIDITY = "is_humidity" CONF_IS_HUMIDITY = "is_humidity"
CONF_IS_GAS = "is_gas"
CONF_IS_ILLUMINANCE = "is_illuminance" CONF_IS_ILLUMINANCE = "is_illuminance"
CONF_IS_POWER = "is_power" CONF_IS_POWER = "is_power"
CONF_IS_POWER_FACTOR = "is_power_factor" CONF_IS_POWER_FACTOR = "is_power_factor"
@ -61,6 +63,7 @@ ENTITY_CONDITIONS = {
DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_IS_CO2}], DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_IS_CO2}],
DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}],
DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}],
DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}],
DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}],
DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}],
DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}],
@ -83,6 +86,7 @@ CONDITION_SCHEMA = vol.All(
CONF_IS_CO2, CONF_IS_CO2,
CONF_IS_CURRENT, CONF_IS_CURRENT,
CONF_IS_ENERGY, CONF_IS_ENERGY,
CONF_IS_GAS,
CONF_IS_HUMIDITY, CONF_IS_HUMIDITY,
CONF_IS_ILLUMINANCE, CONF_IS_ILLUMINANCE,
CONF_IS_POWER, CONF_IS_POWER,

View File

@ -19,6 +19,7 @@ from homeassistant.const import (
DEVICE_CLASS_CO2, DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
@ -44,6 +45,7 @@ CONF_CO = "carbon_monoxide"
CONF_CO2 = "carbon_dioxide" CONF_CO2 = "carbon_dioxide"
CONF_CURRENT = "current" CONF_CURRENT = "current"
CONF_ENERGY = "energy" CONF_ENERGY = "energy"
CONF_GAS = "gas"
CONF_HUMIDITY = "humidity" CONF_HUMIDITY = "humidity"
CONF_ILLUMINANCE = "illuminance" CONF_ILLUMINANCE = "illuminance"
CONF_POWER = "power" CONF_POWER = "power"
@ -60,6 +62,7 @@ ENTITY_TRIGGERS = {
DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_CO2}], DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_CO2}],
DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}], DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}],
DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}], DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}],
DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}],
DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}],
DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}],
DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}],
@ -83,6 +86,7 @@ TRIGGER_SCHEMA = vol.All(
CONF_CO2, CONF_CO2,
CONF_CURRENT, CONF_CURRENT,
CONF_ENERGY, CONF_ENERGY,
CONF_GAS,
CONF_HUMIDITY, CONF_HUMIDITY,
CONF_ILLUMINANCE, CONF_ILLUMINANCE,
CONF_POWER, CONF_POWER,

View File

@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_MONETARY, DEVICE_CLASS_MONETARY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
@ -35,11 +36,14 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
TEMP_KELVIN, TEMP_KELVIN,
VOLUME_CUBIC_FEET,
VOLUME_CUBIC_METERS,
) )
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import homeassistant.util.pressure as pressure_util import homeassistant.util.pressure as pressure_util
import homeassistant.util.temperature as temperature_util import homeassistant.util.temperature as temperature_util
import homeassistant.util.volume as volume_util
from . import ATTR_LAST_RESET, DOMAIN from . import ATTR_LAST_RESET, DOMAIN
@ -53,6 +57,7 @@ DEVICE_CLASS_OR_UNIT_STATISTICS = {
DEVICE_CLASS_POWER: {"mean", "min", "max"}, DEVICE_CLASS_POWER: {"mean", "min", "max"},
DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_PRESSURE: {"mean", "min", "max"},
DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"},
DEVICE_CLASS_GAS: {"sum"},
PERCENTAGE: {"mean", "min", "max"}, PERCENTAGE: {"mean", "min", "max"},
} }
@ -62,6 +67,7 @@ DEVICE_CLASS_UNITS = {
DEVICE_CLASS_POWER: POWER_WATT, DEVICE_CLASS_POWER: POWER_WATT,
DEVICE_CLASS_PRESSURE: PRESSURE_PA, DEVICE_CLASS_PRESSURE: PRESSURE_PA,
DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS,
DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS,
} }
UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
@ -92,6 +98,11 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius, TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius,
TEMP_KELVIN: temperature_util.kelvin_to_celsius, TEMP_KELVIN: temperature_util.kelvin_to_celsius,
}, },
# Convert volume to cubic meter
DEVICE_CLASS_GAS: {
VOLUME_CUBIC_METERS: lambda x: x,
VOLUME_CUBIC_FEET: volume_util.cubic_feet_to_cubic_meter,
},
} }
# Keep track of entities for which a warning about unsupported unit has been logged # Keep track of entities for which a warning about unsupported unit has been logged

View File

@ -5,6 +5,7 @@
"is_battery_level": "Current {entity_name} battery level", "is_battery_level": "Current {entity_name} battery level",
"is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level",
"is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level",
"is_gas": "Current {entity_name} gas",
"is_humidity": "Current {entity_name} humidity", "is_humidity": "Current {entity_name} humidity",
"is_illuminance": "Current {entity_name} illuminance", "is_illuminance": "Current {entity_name} illuminance",
"is_power": "Current {entity_name} power", "is_power": "Current {entity_name} power",
@ -21,6 +22,7 @@
"battery_level": "{entity_name} battery level changes", "battery_level": "{entity_name} battery level changes",
"carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes",
"carbon_dioxide": "{entity_name} carbon dioxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes",
"gas": "{entity_name} gas changes",
"humidity": "{entity_name} humidity changes", "humidity": "{entity_name} humidity changes",
"illuminance": "{entity_name} illuminance changes", "illuminance": "{entity_name} illuminance changes",
"power": "{entity_name} power changes", "power": "{entity_name} power changes",

View File

@ -18,10 +18,12 @@ from homeassistant.const import (
ATTR_ICON, ATTR_ICON,
ATTR_NAME, ATTR_NAME,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_GAS,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
PERCENTAGE, PERCENTAGE,
POWER_WATT, POWER_WATT,
TEMP_CELSIUS, TEMP_CELSIUS,
VOLUME_CUBIC_METERS,
) )
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -38,7 +40,6 @@ DEFAULT_MIN_TEMP = 6.0
CURRENCY_EUR = "EUR" CURRENCY_EUR = "EUR"
VOLUME_CM3 = "CM3" VOLUME_CM3 = "CM3"
VOLUME_M3 = "M3"
VOLUME_LHOUR = "L/H" VOLUME_LHOUR = "L/H"
VOLUME_LMIN = "L/MIN" VOLUME_LMIN = "L/MIN"
@ -125,7 +126,8 @@ SENSOR_ENTITIES = {
ATTR_NAME: "Average Daily Gas Usage", ATTR_NAME: "Average Daily Gas Usage",
ATTR_SECTION: "gas_usage", ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "day_average", ATTR_MEASUREMENT: "day_average",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS,
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
ATTR_ICON: "mdi:gas-cylinder", ATTR_ICON: "mdi:gas-cylinder",
ATTR_DEFAULT_ENABLED: False, ATTR_DEFAULT_ENABLED: False,
}, },
@ -133,7 +135,8 @@ SENSOR_ENTITIES = {
ATTR_NAME: "Gas Usage Today", ATTR_NAME: "Gas Usage Today",
ATTR_SECTION: "gas_usage", ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "day_usage", ATTR_MEASUREMENT: "day_usage",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS,
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
ATTR_ICON: "mdi:gas-cylinder", ATTR_ICON: "mdi:gas-cylinder",
}, },
"gas_daily_cost": { "gas_daily_cost": {
@ -147,9 +150,10 @@ SENSOR_ENTITIES = {
ATTR_NAME: "Gas Meter", ATTR_NAME: "Gas Meter",
ATTR_SECTION: "gas_usage", ATTR_SECTION: "gas_usage",
ATTR_MEASUREMENT: "meter", ATTR_MEASUREMENT: "meter",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
ATTR_ICON: "mdi:gas-cylinder", ATTR_ICON: "mdi:gas-cylinder",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
ATTR_DEVICE_CLASS: DEVICE_CLASS_GAS,
ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_LAST_RESET: dt_util.utc_from_timestamp(0),
ATTR_DEFAULT_ENABLED: False, ATTR_DEFAULT_ENABLED: False,
}, },
@ -321,7 +325,7 @@ SENSOR_ENTITIES = {
ATTR_NAME: "Average Daily Water Usage", ATTR_NAME: "Average Daily Water Usage",
ATTR_SECTION: "water_usage", ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "day_average", ATTR_MEASUREMENT: "day_average",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
ATTR_ICON: "mdi:water", ATTR_ICON: "mdi:water",
ATTR_DEFAULT_ENABLED: False, ATTR_DEFAULT_ENABLED: False,
}, },
@ -329,7 +333,7 @@ SENSOR_ENTITIES = {
ATTR_NAME: "Water Usage Today", ATTR_NAME: "Water Usage Today",
ATTR_SECTION: "water_usage", ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "day_usage", ATTR_MEASUREMENT: "day_usage",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
ATTR_ICON: "mdi:water", ATTR_ICON: "mdi:water",
ATTR_DEFAULT_ENABLED: False, ATTR_DEFAULT_ENABLED: False,
}, },
@ -337,7 +341,7 @@ SENSOR_ENTITIES = {
ATTR_NAME: "Water Meter", ATTR_NAME: "Water Meter",
ATTR_SECTION: "water_usage", ATTR_SECTION: "water_usage",
ATTR_MEASUREMENT: "meter", ATTR_MEASUREMENT: "meter",
ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
ATTR_ICON: "mdi:water", ATTR_ICON: "mdi:water",
ATTR_DEFAULT_ENABLED: False, ATTR_DEFAULT_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,

View File

@ -247,6 +247,7 @@ DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength"
DEVICE_CLASS_TEMPERATURE: Final = "temperature" DEVICE_CLASS_TEMPERATURE: Final = "temperature"
DEVICE_CLASS_TIMESTAMP: Final = "timestamp" DEVICE_CLASS_TIMESTAMP: Final = "timestamp"
DEVICE_CLASS_VOLTAGE: Final = "voltage" DEVICE_CLASS_VOLTAGE: Final = "voltage"
DEVICE_CLASS_GAS: Final = "gas"
# #### STATES #### # #### STATES ####
STATE_ON: Final = "on" STATE_ON: Final = "on"

View File

@ -6,6 +6,8 @@ from numbers import Number
from homeassistant.const import ( from homeassistant.const import (
UNIT_NOT_RECOGNIZED_TEMPLATE, UNIT_NOT_RECOGNIZED_TEMPLATE,
VOLUME, VOLUME,
VOLUME_CUBIC_FEET,
VOLUME_CUBIC_METERS,
VOLUME_FLUID_OUNCE, VOLUME_FLUID_OUNCE,
VOLUME_GALLONS, VOLUME_GALLONS,
VOLUME_LITERS, VOLUME_LITERS,
@ -17,19 +19,31 @@ VALID_UNITS: tuple[str, ...] = (
VOLUME_MILLILITERS, VOLUME_MILLILITERS,
VOLUME_GALLONS, VOLUME_GALLONS,
VOLUME_FLUID_OUNCE, VOLUME_FLUID_OUNCE,
VOLUME_CUBIC_METERS,
VOLUME_CUBIC_FEET,
) )
def __liter_to_gallon(liter: float) -> float: def liter_to_gallon(liter: float) -> float:
"""Convert a volume measurement in Liter to Gallon.""" """Convert a volume measurement in Liter to Gallon."""
return liter * 0.2642 return liter * 0.2642
def __gallon_to_liter(gallon: float) -> float: def gallon_to_liter(gallon: float) -> float:
"""Convert a volume measurement in Gallon to Liter.""" """Convert a volume measurement in Gallon to Liter."""
return gallon * 3.785 return gallon * 3.785
def cubic_meter_to_cubic_feet(cubic_meter: float) -> float:
"""Convert a volume measurement in cubic meter to cubic feet."""
return cubic_meter * 35.3146667
def cubic_feet_to_cubic_meter(cubic_feet: float) -> float:
"""Convert a volume measurement in cubic feet to cubic meter."""
return cubic_feet * 0.0283168466
def convert(volume: float, from_unit: str, to_unit: str) -> float: def convert(volume: float, from_unit: str, to_unit: str) -> float:
"""Convert a temperature from one unit to another.""" """Convert a temperature from one unit to another."""
if from_unit not in VALID_UNITS: if from_unit not in VALID_UNITS:
@ -45,8 +59,12 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float:
result: float = volume result: float = volume
if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS: if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS:
result = __liter_to_gallon(volume) result = liter_to_gallon(volume)
elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS: elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS:
result = __gallon_to_liter(volume) result = gallon_to_liter(volume)
elif from_unit == VOLUME_CUBIC_METERS and to_unit == VOLUME_CUBIC_FEET:
result = cubic_meter_to_cubic_feet(volume)
elif from_unit == VOLUME_CUBIC_FEET and to_unit == VOLUME_CUBIC_METERS:
result = cubic_feet_to_cubic_meter(volume)
return result return result

View File

@ -24,6 +24,7 @@ from homeassistant.const import (
ATTR_ICON, ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
STATE_UNKNOWN, STATE_UNKNOWN,
@ -104,7 +105,7 @@ async def test_default_setup(hass, dsmr_connection_fixture):
GAS_METER_READING: MBusObject( GAS_METER_READING: MBusObject(
[ [
{"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, {"value": Decimal(745.695), "unit": "m3"},
] ]
), ),
} }
@ -164,7 +165,7 @@ async def test_default_setup(hass, dsmr_connection_fixture):
# check if gas consumption is parsed correctly # check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption") gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695" assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS
assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -228,7 +229,7 @@ async def test_v4_meter(hass, dsmr_connection_fixture):
HOURLY_GAS_METER_READING: MBusObject( HOURLY_GAS_METER_READING: MBusObject(
[ [
{"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, {"value": Decimal(745.695), "unit": "m3"},
] ]
), ),
ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
@ -263,8 +264,8 @@ async def test_v4_meter(hass, dsmr_connection_fixture):
# check if gas consumption is parsed correctly # check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption") gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695" assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None
assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -299,7 +300,7 @@ async def test_v5_meter(hass, dsmr_connection_fixture):
HOURLY_GAS_METER_READING: MBusObject( HOURLY_GAS_METER_READING: MBusObject(
[ [
{"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, {"value": Decimal(745.695), "unit": "m3"},
] ]
), ),
ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
@ -334,7 +335,7 @@ async def test_v5_meter(hass, dsmr_connection_fixture):
# check if gas consumption is parsed correctly # check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption") gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695" assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS
assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -370,7 +371,7 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture):
HOURLY_GAS_METER_READING: MBusObject( HOURLY_GAS_METER_READING: MBusObject(
[ [
{"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, {"value": Decimal(745.695), "unit": "m3"},
] ]
), ),
LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject(
@ -415,7 +416,7 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture):
# check if gas consumption is parsed correctly # check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption") gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695" assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS
assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
@ -450,7 +451,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture):
BELGIUM_HOURLY_GAS_METER_READING: MBusObject( BELGIUM_HOURLY_GAS_METER_READING: MBusObject(
[ [
{"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, {"value": Decimal(745.695), "unit": "m3"},
] ]
), ),
ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
@ -485,7 +486,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture):
# check if gas consumption is parsed correctly # check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption") gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695" assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is DEVICE_CLASS_GAS
assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire"
assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None
assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT

View File

@ -86,7 +86,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat
if device_class != "none" if device_class != "none"
] ]
triggers = await async_get_device_automations(hass, "trigger", device_entry.id) triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
assert len(triggers) == 13 assert len(triggers) == 14
assert triggers == expected_triggers assert triggers == expected_triggers

View File

@ -39,6 +39,11 @@ TEMPERATURE_SENSOR_ATTRIBUTES = {
"state_class": "measurement", "state_class": "measurement",
"unit_of_measurement": "°C", "unit_of_measurement": "°C",
} }
GAS_SENSOR_ATTRIBUTES = {
"device_class": "gas",
"state_class": "measurement",
"unit_of_measurement": "",
}
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -154,11 +159,13 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
[ [
("energy", "kWh", "kWh", 1), ("energy", "kWh", "kWh", 1),
("energy", "Wh", "kWh", 1 / 1000), ("energy", "Wh", "kWh", 1 / 1000),
("monetary", "", "", 1), ("monetary", "EUR", "EUR", 1),
("monetary", "SEK", "SEK", 1), ("monetary", "SEK", "SEK", 1),
("gas", "", "", 1),
("gas", "ft³", "", 0.0283168466),
], ],
) )
def test_compile_hourly_energy_statistics( def test_compile_hourly_sum_statistics(
hass_recorder, caplog, device_class, unit, native_unit, factor hass_recorder, caplog, device_class, unit, native_unit, factor
): ):
"""Test compiling hourly statistics.""" """Test compiling hourly statistics."""
@ -174,7 +181,7 @@ def test_compile_hourly_energy_statistics(
} }
seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] seq = [10, 15, 20, 10, 30, 40, 50, 60, 70]
four, eight, states = record_energy_states( four, eight, states = record_meter_states(
hass, zero, "sensor.test1", attributes, seq hass, zero, "sensor.test1", attributes, seq
) )
hist = history.get_significant_states( hist = history.get_significant_states(
@ -254,14 +261,14 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog):
seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
four, eight, states = record_energy_states( four, eight, states = record_meter_states(
hass, zero, "sensor.test1", sns1_attr, seq1 hass, zero, "sensor.test1", sns1_attr, seq1
) )
_, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) _, _, _states = record_meter_states(hass, zero, "sensor.test2", sns2_attr, seq2)
states = {**states, **_states} states = {**states, **_states}
_, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) _, _, _states = record_meter_states(hass, zero, "sensor.test3", sns3_attr, seq3)
states = {**states, **_states} states = {**states, **_states}
_, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) _, _, _states = record_meter_states(hass, zero, "sensor.test4", sns4_attr, seq4)
states = {**states, **_states} states = {**states, **_states}
hist = history.get_significant_states( hist = history.get_significant_states(
@ -336,14 +343,14 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog):
seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90]
four, eight, states = record_energy_states( four, eight, states = record_meter_states(
hass, zero, "sensor.test1", sns1_attr, seq1 hass, zero, "sensor.test1", sns1_attr, seq1
) )
_, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) _, _, _states = record_meter_states(hass, zero, "sensor.test2", sns2_attr, seq2)
states = {**states, **_states} states = {**states, **_states}
_, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) _, _, _states = record_meter_states(hass, zero, "sensor.test3", sns3_attr, seq3)
states = {**states, **_states} states = {**states, **_states}
_, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) _, _, _states = record_meter_states(hass, zero, "sensor.test4", sns4_attr, seq4)
states = {**states, **_states} states = {**states, **_states}
hist = history.get_significant_states( hist = history.get_significant_states(
hass, zero - timedelta.resolution, eight + timedelta.resolution hass, zero - timedelta.resolution, eight + timedelta.resolution
@ -632,6 +639,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog):
("humidity", None, None, "mean"), ("humidity", None, None, "mean"),
("monetary", "USD", "USD", "sum"), ("monetary", "USD", "USD", "sum"),
("monetary", "None", "None", "sum"), ("monetary", "None", "None", "sum"),
("gas", "", "", "sum"),
("gas", "ft³", "", "sum"),
("pressure", "Pa", "Pa", "mean"), ("pressure", "Pa", "Pa", "mean"),
("pressure", "hPa", "Pa", "mean"), ("pressure", "hPa", "Pa", "mean"),
("pressure", "mbar", "Pa", "mean"), ("pressure", "mbar", "Pa", "mean"),
@ -697,7 +706,7 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes):
def record_states(hass, zero, entity_id, attributes): def record_states(hass, zero, entity_id, attributes):
"""Record some test states. """Record some test states.
We inject a bunch of state updates for temperature sensors. We inject a bunch of state updates for measurement sensors.
""" """
attributes = dict(attributes) attributes = dict(attributes)
@ -725,10 +734,10 @@ def record_states(hass, zero, entity_id, attributes):
return four, states return four, states
def record_energy_states(hass, zero, entity_id, _attributes, seq): def record_meter_states(hass, zero, entity_id, _attributes, seq):
"""Record some test states. """Record some test states.
We inject a bunch of state updates for energy sensors. We inject a bunch of state updates for meter sensors.
""" """
def set_state(entity_id, state, **kwargs): def set_state(entity_id, state, **kwargs):

View File

@ -9,6 +9,7 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
PRESSURE_HPA, PRESSURE_HPA,
SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS,
VOLUME_CUBIC_METERS,
) )
from tests.common import MockEntity from tests.common import MockEntity
@ -30,6 +31,7 @@ UNITS_OF_MEASUREMENT = {
sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh) sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh)
sensor.DEVICE_CLASS_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) sensor.DEVICE_CLASS_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0)
sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V) sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V)
sensor.DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS, # gas (m³)
} }
ENTITIES = {} ENTITIES = {}

View File

@ -3,6 +3,8 @@
import pytest import pytest
from homeassistant.const import ( from homeassistant.const import (
VOLUME_CUBIC_FEET,
VOLUME_CUBIC_METERS,
VOLUME_FLUID_OUNCE, VOLUME_FLUID_OUNCE,
VOLUME_GALLONS, VOLUME_GALLONS,
VOLUME_LITERS, VOLUME_LITERS,
@ -47,3 +49,21 @@ def test_convert_from_gallons():
"""Test conversion from gallons to other units.""" """Test conversion from gallons to other units."""
gallons = 5 gallons = 5
assert volume_util.convert(gallons, VOLUME_GALLONS, VOLUME_LITERS) == 18.925 assert volume_util.convert(gallons, VOLUME_GALLONS, VOLUME_LITERS) == 18.925
def test_convert_from_cubic_meters():
"""Test conversion from cubic meter to other units."""
cubic_meters = 5
assert (
volume_util.convert(cubic_meters, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET)
== 176.5733335
)
def test_convert_from_cubic_feet():
"""Test conversion from cubic feet to cubic meters to other units."""
cubic_feets = 500
assert (
volume_util.convert(cubic_feets, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)
== 14.1584233
)