mirror of
https://github.com/esphome/esphome.git
synced 2026-10-11 22:04:16 +00:00
[teleinfo] Add predefined properties for main tags of TIC standard and historical (#9167)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
Claude Opus 4.6
J. Nick Koston
parent
f9a81869ef
commit
721228c1d2
+1
-1
@@ -570,7 +570,7 @@ esphome/components/tca9555/* @mobrembski
|
||||
esphome/components/tcl112/* @glmnet
|
||||
esphome/components/tcp_uart/* @Bascht74
|
||||
esphome/components/tee501/* @Stock-M
|
||||
esphome/components/teleinfo/* @0hax
|
||||
esphome/components/teleinfo/* @0hax @FredM67
|
||||
esphome/components/tem3200/* @bakerkj
|
||||
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
esphome/components/template/datetime/* @rfdarter
|
||||
|
||||
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@0hax"]
|
||||
CODEOWNERS = ["@0hax", "@FredM67"]
|
||||
DOMAIN = "teleinfo"
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_FLASH,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOWATT,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MILLIAMP,
|
||||
UNIT_MILLIVOLT,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
@@ -13,19 +35,125 @@ from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, telein
|
||||
|
||||
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
TeleInfoSensor,
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
icon=ICON_FLASH,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
).extend(TELEINFO_LISTENER_SCHEMA)
|
||||
|
||||
def _preset(unit: str, device_class: str, state_class: str) -> dict[str, str]:
|
||||
return {
|
||||
CONF_UNIT_OF_MEASUREMENT: unit,
|
||||
CONF_DEVICE_CLASS: device_class,
|
||||
CONF_STATE_CLASS: state_class,
|
||||
}
|
||||
|
||||
|
||||
_ENERGY = _preset(UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING)
|
||||
_REACTIVE_ENERGY = _preset(
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
)
|
||||
_CURRENT = _preset(UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT)
|
||||
_VOLTAGE = _preset(UNIT_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT)
|
||||
_APPARENT_POWER_VA = _preset(
|
||||
UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_APPARENT_POWER_KVA = _preset(
|
||||
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
|
||||
|
||||
# Legacy defaults, kept for tags without a known preset
|
||||
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
|
||||
|
||||
# Units that are valid for each device class, used to decide whether a preset's
|
||||
# device class can be kept when the user overrides the unit (and vice versa)
|
||||
_COMPATIBLE_UNITS = {
|
||||
DEVICE_CLASS_ENERGY: {UNIT_WATT_HOURS, UNIT_KILOWATT_HOURS},
|
||||
DEVICE_CLASS_REACTIVE_ENERGY: {
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
},
|
||||
DEVICE_CLASS_CURRENT: {UNIT_AMPERE, UNIT_MILLIAMP},
|
||||
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
|
||||
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
|
||||
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
|
||||
}
|
||||
|
||||
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
|
||||
TIC_TAG_CONFIGS = {
|
||||
# Standard mode
|
||||
"EA": _ENERGY,
|
||||
"ER": _REACTIVE_ENERGY,
|
||||
"IRMS": _CURRENT,
|
||||
"URMS": _VOLTAGE,
|
||||
"UMOY": _VOLTAGE,
|
||||
"SINST": _APPARENT_POWER_VA,
|
||||
"SMAX": _APPARENT_POWER_VA,
|
||||
"CC": _POWER,
|
||||
"PREF": _APPARENT_POWER_KVA,
|
||||
"PCOUP": _APPARENT_POWER_KVA,
|
||||
# Historical mode
|
||||
"BASE": _ENERGY,
|
||||
"HCH": _ENERGY,
|
||||
"EJP": _ENERGY,
|
||||
"BBRH": _ENERGY,
|
||||
"IINST": _CURRENT,
|
||||
"IMAX": _CURRENT,
|
||||
"ISOUSC": _CURRENT,
|
||||
"ADPS": _CURRENT,
|
||||
"ADIR": _CURRENT,
|
||||
"PAPP": _APPARENT_POWER_VA,
|
||||
"PMAX": _POWER,
|
||||
}
|
||||
|
||||
|
||||
def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
"""Apply preset configurations based on the tag name.
|
||||
|
||||
Only keys not set by the user are filled in. For known tags, a preset unit or
|
||||
device class is dropped if it is incompatible with the one set by the user.
|
||||
Unknown tags get the legacy defaults, merged key by key as before.
|
||||
"""
|
||||
if CONF_TAG_NAME not in config:
|
||||
return config
|
||||
|
||||
# Runs before cv.string, so the raw value may not be a string yet
|
||||
tag_name = str(config[CONF_TAG_NAME])
|
||||
preset = next(
|
||||
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
|
||||
None,
|
||||
)
|
||||
|
||||
config = dict(config)
|
||||
if preset is None:
|
||||
for key, value in _LEGACY_DEFAULT.items():
|
||||
config.setdefault(key, value)
|
||||
return config
|
||||
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
|
||||
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
|
||||
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
|
||||
|
||||
for key, value in preset.items():
|
||||
if key in config:
|
||||
continue
|
||||
if key in (CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE_CLASS) and not compatible:
|
||||
continue
|
||||
config[key] = value
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
apply_tag_config,
|
||||
sensor.sensor_schema(
|
||||
TeleInfoSensor,
|
||||
accuracy_decimals=0,
|
||||
).extend(TELEINFO_LISTENER_SCHEMA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID])
|
||||
cg.add(teleinfo.register_teleinfo_listener(var))
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Tests for teleinfo sensor tag presets."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.teleinfo import CONF_TAG_NAME
|
||||
from esphome.components.teleinfo.sensor import apply_tag_config
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ICON,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_FLASH,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MILLIAMP,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag", "unit", "device_class", "state_class"),
|
||||
[
|
||||
("EAST", UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING),
|
||||
(
|
||||
"ERQ1",
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
("IRMS1", UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT),
|
||||
("URMS1", UNIT_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT),
|
||||
(
|
||||
"SINSTS",
|
||||
UNIT_VOLT_AMPS,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
(
|
||||
"PREF",
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
("CCASN", UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT),
|
||||
("HCHC", UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING),
|
||||
("BBRHCJB", UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING),
|
||||
("IINST1", UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT),
|
||||
("ADIR2", UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT),
|
||||
("PAPP", UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT),
|
||||
("PMAX", UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT),
|
||||
],
|
||||
)
|
||||
def test_known_tag_preset(
|
||||
tag: str, unit: str, device_class: str, state_class: str
|
||||
) -> None:
|
||||
config = apply_tag_config({CONF_TAG_NAME: tag})
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == unit
|
||||
assert config[CONF_DEVICE_CLASS] == device_class
|
||||
assert config[CONF_STATE_CLASS] == state_class
|
||||
assert CONF_ICON not in config
|
||||
|
||||
|
||||
def test_unknown_tag_keeps_legacy_defaults() -> None:
|
||||
config = apply_tag_config({CONF_TAG_NAME: "MYTAG"})
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == UNIT_WATT_HOURS
|
||||
assert config[CONF_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
assert config[CONF_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
||||
assert config[CONF_ICON] == ICON_FLASH
|
||||
|
||||
|
||||
def test_unknown_tag_user_unit_keeps_legacy_device_class() -> None:
|
||||
config = apply_tag_config(
|
||||
{CONF_TAG_NAME: "MYTAG", CONF_UNIT_OF_MEASUREMENT: UNIT_KILOWATT_HOURS}
|
||||
)
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == UNIT_KILOWATT_HOURS
|
||||
assert config[CONF_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
assert config[CONF_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
||||
assert config[CONF_ICON] == ICON_FLASH
|
||||
|
||||
|
||||
def test_unknown_tag_starting_with_u_is_not_voltage() -> None:
|
||||
config = apply_tag_config({CONF_TAG_NAME: "UNKNOWN"})
|
||||
assert config[CONF_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
|
||||
|
||||
def test_numeric_tag_name_does_not_crash() -> None:
|
||||
config = apply_tag_config({CONF_TAG_NAME: 1234})
|
||||
assert config[CONF_TAG_NAME] == 1234
|
||||
assert config[CONF_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag", "unit", "device_class"),
|
||||
[
|
||||
("HCHP", UNIT_KILOWATT_HOURS, DEVICE_CLASS_ENERGY),
|
||||
("EAST", UNIT_KILOWATT_HOURS, DEVICE_CLASS_ENERGY),
|
||||
("PREF", UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER),
|
||||
("IINST1", UNIT_MILLIAMP, DEVICE_CLASS_CURRENT),
|
||||
],
|
||||
)
|
||||
def test_user_compatible_unit_keeps_preset_device_class(
|
||||
tag: str, unit: str, device_class: str
|
||||
) -> None:
|
||||
config = apply_tag_config({CONF_TAG_NAME: tag, CONF_UNIT_OF_MEASUREMENT: unit})
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == unit
|
||||
assert config[CONF_DEVICE_CLASS] == device_class
|
||||
|
||||
|
||||
def test_user_incompatible_unit_skips_preset_device_class() -> None:
|
||||
config = apply_tag_config({CONF_TAG_NAME: "PAPP", CONF_UNIT_OF_MEASUREMENT: "W"})
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == "W"
|
||||
assert CONF_DEVICE_CLASS not in config
|
||||
assert config[CONF_STATE_CLASS] == STATE_CLASS_MEASUREMENT
|
||||
|
||||
|
||||
def test_user_incompatible_device_class_skips_preset_unit() -> None:
|
||||
config = apply_tag_config(
|
||||
{CONF_TAG_NAME: "PAPP", CONF_DEVICE_CLASS: DEVICE_CLASS_POWER}
|
||||
)
|
||||
assert config[CONF_DEVICE_CLASS] == DEVICE_CLASS_POWER
|
||||
assert CONF_UNIT_OF_MEASUREMENT not in config
|
||||
|
||||
|
||||
def test_user_same_device_class_keeps_preset_unit() -> None:
|
||||
config = apply_tag_config(
|
||||
{CONF_TAG_NAME: "PAPP", CONF_DEVICE_CLASS: DEVICE_CLASS_APPARENT_POWER}
|
||||
)
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == UNIT_VOLT_AMPS
|
||||
|
||||
|
||||
def test_user_state_class_is_kept() -> None:
|
||||
config = apply_tag_config(
|
||||
{CONF_TAG_NAME: "HCHP", CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT}
|
||||
)
|
||||
assert config[CONF_STATE_CLASS] == STATE_CLASS_MEASUREMENT
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == UNIT_WATT_HOURS
|
||||
@@ -22,11 +22,95 @@ teleinfo:
|
||||
update_interval: 60s
|
||||
|
||||
sensor:
|
||||
# Historical mode tags - auto-configured via apply_tag_config
|
||||
- platform: teleinfo
|
||||
name: hchc
|
||||
tag_name: HCHC
|
||||
teleinfo_id: test_teleinfo
|
||||
unit_of_measurement: Wh
|
||||
- platform: teleinfo
|
||||
name: base
|
||||
tag_name: BASE
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: iinst1
|
||||
tag_name: IINST1
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: papp
|
||||
tag_name: PAPP
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: bbrhcjb
|
||||
tag_name: BBRHCJB
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: ejphn
|
||||
tag_name: EJPHN
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: pmax
|
||||
tag_name: PMAX
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: imax1
|
||||
tag_name: IMAX1
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: isousc
|
||||
tag_name: ISOUSC
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: adps
|
||||
tag_name: ADPS
|
||||
teleinfo_id: test_teleinfo
|
||||
# Standard mode tags - auto-configured via apply_tag_config
|
||||
- platform: teleinfo
|
||||
name: east
|
||||
tag_name: EAST
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: erqt
|
||||
tag_name: ERQT
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: irms1
|
||||
tag_name: IRMS1
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: urms1
|
||||
tag_name: URMS1
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: sinsts
|
||||
tag_name: SINSTS
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: smaxsn
|
||||
tag_name: SMAXSN
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: ccasn
|
||||
tag_name: CCASN
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: pref
|
||||
tag_name: PREF
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: pcoup
|
||||
tag_name: PCOUP
|
||||
teleinfo_id: test_teleinfo
|
||||
# Explicit override - user-specified unit should not be overwritten
|
||||
- platform: teleinfo
|
||||
name: hchp_override
|
||||
tag_name: HCHP
|
||||
teleinfo_id: test_teleinfo
|
||||
unit_of_measurement: kWh
|
||||
# Unknown tag - no auto-config match, should still compile
|
||||
- platform: teleinfo
|
||||
name: custom_tag
|
||||
tag_name: MYTAG
|
||||
teleinfo_id: test_teleinfo
|
||||
|
||||
text_sensor:
|
||||
- platform: teleinfo
|
||||
|
||||
Reference in New Issue
Block a user