diff --git a/esphome/components/dlms_meter/__init__.py b/esphome/components/dlms_meter/__init__.py index 6d86da6ccf..03aca53711 100644 --- a/esphome/components/dlms_meter/__init__.py +++ b/esphome/components/dlms_meter/__init__.py @@ -172,43 +172,17 @@ async def to_code(config: ConfigType) -> None: hub_id = config[CONF_ID].id - sensor_count = 0 - for sens_conf in CORE.config.get(SENSOR_DOMAIN, []): - if ( - sens_conf.get("platform") == DOMAIN - and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id - ): - if CONF_OBIS_CODE in sens_conf: - sensor_count += 1 - else: - from .sensor import NUMERIC_KEYS - - sensor_count += sum(1 for key in NUMERIC_KEYS if key in sens_conf) - - text_sensor_count = 0 - for sens_conf in CORE.config.get(TEXT_SENSOR_DOMAIN, []): - if ( - sens_conf.get("platform") == DOMAIN - and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id - ): - if CONF_OBIS_CODE in sens_conf: - text_sensor_count += 1 - else: - from .text_sensor import TEXT_KEYS - - text_sensor_count += sum(1 for key in TEXT_KEYS if key in sens_conf) - - binary_sensor_count = 0 - for sens_conf in CORE.config.get(BINARY_SENSOR_DOMAIN, []): - if ( - sens_conf.get("platform") == DOMAIN - and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id - ): - binary_sensor_count += 1 - - cg.add_define("DLMS_MAX_SENSORS", sensor_count) - cg.add_define("DLMS_MAX_TEXT_SENSORS", text_sensor_count) - cg.add_define("DLMS_MAX_BINARY_SENSORS", binary_sensor_count) + for domain, define in ( + (SENSOR_DOMAIN, "DLMS_MAX_SENSORS"), + (TEXT_SENSOR_DOMAIN, "DLMS_MAX_TEXT_SENSORS"), + (BINARY_SENSOR_DOMAIN, "DLMS_MAX_BINARY_SENSORS"), + ): + count = sum( + 1 + for conf in CORE.config.get(domain, []) + if conf.get("platform") == DOMAIN and conf[CONF_DLMS_METER_ID].id == hub_id + ) + cg.add_define(define, count) await cg.register_component(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/dlms_meter/sensor/__init__.py b/esphome/components/dlms_meter/sensor/__init__.py index 8ded150cd0..0c895ccefb 100644 --- a/esphome/components/dlms_meter/sensor/__init__.py +++ b/esphome/components/dlms_meter/sensor/__init__.py @@ -1,159 +1,59 @@ -import logging - import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, - UNIT_AMPERE, - UNIT_VOLT, - UNIT_WATT, - UNIT_WATT_HOURS, -) from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code -_LOGGER = logging.getLogger(__name__) - DEPENDENCIES = ["dlms_meter"] -NUMERIC_KEYS = { - "voltage_l1": "1.0.32.7.0.255", - "voltage_l2": "1.0.52.7.0.255", - "voltage_l3": "1.0.72.7.0.255", - "current_l1": "1.0.31.7.0.255", - "current_l2": "1.0.51.7.0.255", - "current_l3": "1.0.71.7.0.255", - "active_power_plus": "1.0.1.7.0.255", - "active_power_minus": "1.0.2.7.0.255", - "active_energy_plus": "1.0.1.8.0.255", - "active_energy_minus": "1.0.2.8.0.255", - "reactive_energy_plus": "1.0.3.8.0.255", - "reactive_energy_minus": "1.0.4.8.0.255", - "power_factor": "1.0.13.7.0.255", +# Removed in 2026.11.0 - kept to provide helpful error message +# Remove before 2027.5.0 +_MEASUREMENT = "state_class: measurement" +_VOLTAGE = f"unit_of_measurement: V, accuracy_decimals: 1, device_class: voltage, {_MEASUREMENT}" +_CURRENT = f"unit_of_measurement: A, accuracy_decimals: 2, device_class: current, {_MEASUREMENT}" +_POWER = ( + f"unit_of_measurement: W, accuracy_decimals: 0, device_class: power, {_MEASUREMENT}" +) +_ENERGY = ( + "unit_of_measurement: Wh, accuracy_decimals: 0, device_class: energy, " + "state_class: total_increasing" +) +_POWER_FACTOR = f"accuracy_decimals: 3, device_class: power_factor, {_MEASUREMENT}" +REMOVED_KEYS = { + "voltage_l1": ("1.0.32.7.0.255", _VOLTAGE), + "voltage_l2": ("1.0.52.7.0.255", _VOLTAGE), + "voltage_l3": ("1.0.72.7.0.255", _VOLTAGE), + "current_l1": ("1.0.31.7.0.255", _CURRENT), + "current_l2": ("1.0.51.7.0.255", _CURRENT), + "current_l3": ("1.0.71.7.0.255", _CURRENT), + "active_power_plus": ("1.0.1.7.0.255", _POWER), + "active_power_minus": ("1.0.2.7.0.255", _POWER), + "active_energy_plus": ("1.0.1.8.0.255", _ENERGY), + "active_energy_minus": ("1.0.2.8.0.255", _ENERGY), + "reactive_energy_plus": ("1.0.3.8.0.255", _ENERGY), + "reactive_energy_minus": ("1.0.4.8.0.255", _ENERGY), + "power_factor": ("1.0.13.7.0.255", _POWER_FACTOR), } -DYNAMIC_SCHEMA = sensor.sensor_schema().extend( + +CONFIG_SCHEMA = sensor.sensor_schema().extend( { cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), cv.Required(CONF_OBIS_CODE): obis_code, + **{ + cv.Optional(key): cv.invalid( + f"The predefined '{key}' key was removed in ESPHome 2026.11.0. " + f"Add a separate '- platform: dlms_meter' sensor with " + f'obis_code: "{obis}" and {settings}' + ) + for key, (obis, settings) in REMOVED_KEYS.items() + }, } ) -def deprecation_warning(config: ConfigType) -> ConfigType: - _LOGGER.warning( - "The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. " - "Please update your configuration to use the new schema with 'obis_code'." - ) - return config - - -OLD_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), - cv.Optional("voltage_l1"): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("voltage_l2"): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("voltage_l3"): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("current_l1"): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("current_l2"): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("current_l3"): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("active_power_plus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("active_power_minus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("active_energy_plus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("active_energy_minus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("reactive_energy_plus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("reactive_energy_minus"): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional("power_factor"): sensor.sensor_schema( - accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, - ), - } - ).extend(cv.COMPONENT_SCHEMA), - deprecation_warning, -) - - -CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) - - async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) - - if obis := config.get(CONF_OBIS_CODE): - var = await sensor.new_sensor(config) - cg.add(hub.register_sensor(obis, var)) - else: - for key, obis_val in NUMERIC_KEYS.items(): - if sensor_config := config.get(key): - sens = await sensor.new_sensor(sensor_config) - cg.add(hub.register_sensor(obis_val, sens)) + var = await sensor.new_sensor(config) + cg.add(hub.register_sensor(config[CONF_OBIS_CODE], var)) diff --git a/esphome/components/dlms_meter/text_sensor/__init__.py b/esphome/components/dlms_meter/text_sensor/__init__.py index c2ff0779ee..10872d1a50 100644 --- a/esphome/components/dlms_meter/text_sensor/__init__.py +++ b/esphome/components/dlms_meter/text_sensor/__init__.py @@ -1,5 +1,3 @@ -import logging - import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv @@ -7,54 +5,33 @@ from esphome.types import ConfigType from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code -_LOGGER = logging.getLogger(__name__) - DEPENDENCIES = ["dlms_meter"] -TEXT_KEYS = { +# Removed in 2026.11.0 - kept to provide helpful error message +# Remove before 2027.5.0 +REMOVED_KEYS = { "timestamp": "0.0.1.0.0.255", "meternumber": "0.0.96.1.0.255", } -DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend( + +CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend( { cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), cv.Required(CONF_OBIS_CODE): obis_code, + **{ + cv.Optional(key): cv.invalid( + f"The predefined '{key}' key was removed in ESPHome 2026.11.0. " + f"Add a separate '- platform: dlms_meter' text sensor with " + f'obis_code: "{obis}" instead' + ) + for key, obis in REMOVED_KEYS.items() + }, } ) -def deprecation_warning(config: ConfigType) -> ConfigType: - _LOGGER.warning( - "The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. " - "Please update your configuration to use the new schema with 'obis_code'." - ) - return config - - -OLD_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), - cv.Optional("timestamp"): text_sensor.text_sensor_schema(), - cv.Optional("meternumber"): text_sensor.text_sensor_schema(), - } - ).extend(cv.COMPONENT_SCHEMA), - deprecation_warning, -) - - -CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA) - - async def to_code(config: ConfigType) -> None: hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) - - if obis := config.get(CONF_OBIS_CODE): - var = await text_sensor.new_text_sensor(config) - cg.add(hub.register_text_sensor(obis, var)) - else: - for key, obis_val in TEXT_KEYS.items(): - if text_sensor_config := config.get(key): - sens = await text_sensor.new_text_sensor(text_sensor_config) - cg.add(hub.register_text_sensor(obis_val, sens)) + var = await text_sensor.new_text_sensor(config) + cg.add(hub.register_text_sensor(config[CONF_OBIS_CODE], var)) diff --git a/tests/component_tests/dlms_meter/test_removed_keys.py b/tests/component_tests/dlms_meter/test_removed_keys.py new file mode 100644 index 0000000000..f82ec79bff --- /dev/null +++ b/tests/component_tests/dlms_meter/test_removed_keys.py @@ -0,0 +1,33 @@ +"""Tests for the dlms_meter predefined keys removed in 2026.11.0.""" + +from collections.abc import Callable + +import pytest +from voluptuous import Invalid, MultipleInvalid + +from esphome.components.dlms_meter.sensor import CONFIG_SCHEMA as SENSOR_SCHEMA +from esphome.components.dlms_meter.text_sensor import ( + CONFIG_SCHEMA as TEXT_SENSOR_SCHEMA, +) +from esphome.types import ConfigType + + +def _errors(schema: Callable[[ConfigType], ConfigType], config: ConfigType) -> str: + with pytest.raises(Invalid) as exc_info: + schema(config) + errors = ( + exc_info.value.errors + if isinstance(exc_info.value, MultipleInvalid) + else [exc_info.value] + ) + return "\n".join(str(error) for error in errors) + + +def test_removed_sensor_key_names_its_replacement() -> None: + errors = _errors(SENSOR_SCHEMA, {"voltage_l1": {"name": "Voltage"}}) + assert 'obis_code: "1.0.32.7.0.255" and unit_of_measurement: V' in errors + + +def test_removed_text_sensor_key_names_its_replacement() -> None: + errors = _errors(TEXT_SENSOR_SCHEMA, {"timestamp": {"name": "Timestamp"}}) + assert 'obis_code: "0.0.1.0.0.255"' in errors diff --git a/tests/components/dlms_meter/common.yaml b/tests/components/dlms_meter/common.yaml index 4d32f4e02f..fa6deff6be 100644 --- a/tests/components/dlms_meter/common.yaml +++ b/tests/components/dlms_meter/common.yaml @@ -15,57 +15,36 @@ dlms_meter: default_obis: "0.0.1.0.0.255" sensor: - # Old Schema tests - platform: dlms_meter - voltage_l1: - name: "Voltage L1" - voltage_l2: - name: "Voltage L2" - voltage_l3: - name: "Voltage L3" - current_l1: - name: "Current L1" - current_l2: - name: "Current L2" - current_l3: - name: "Current L3" - active_power_plus: - name: "Active power taken from grid" - active_power_minus: - name: "Active power put into grid" - active_energy_plus: - name: "Active energy taken from grid" - active_energy_minus: - name: "Active energy put into grid" - reactive_energy_plus: - name: "Reactive energy taken from grid" - reactive_energy_minus: - name: "Reactive energy put into grid" - power_factor: - name: "Power factor" + obis_code: "1.0.32.7.0.255" + name: "Voltage L1" + unit_of_measurement: V + accuracy_decimals: 1 + device_class: voltage + state_class: measurement + - platform: dlms_meter + obis_code: "1.0.1.8.0.255" + name: "Active energy taken from grid" + unit_of_measurement: Wh + device_class: energy + state_class: total_increasing - # Dynamic Schema tests - platform: dlms_meter dlms_meter_id: dlms_meter_hub obis_code: "1-0:99.99.9" name: "Custom Dynamic Sensor" text_sensor: - # Old Schema tests - platform: dlms_meter - timestamp: - name: "timestamp" - meternumber: - name: "Meter Number" + obis_code: "0.0.1.0.0.255" + name: "timestamp" - # Dynamic Schema tests - platform: dlms_meter dlms_meter_id: dlms_meter_hub obis_code: "0-0:99.99.9" name: "Custom Dynamic Text Sensor" binary_sensor: - # Dynamic Schema tests (Binary sensors only use the dynamic schema) - platform: dlms_meter dlms_meter_id: dlms_meter_hub obis_code: "0-1:2.3.4"