diff --git a/pylint/plugins/hass_enforce_greek_micro_char.py b/pylint/plugins/hass_enforce_greek_micro_char.py index bee6efe65dc..2cd8dc0d802 100644 --- a/pylint/plugins/hass_enforce_greek_micro_char.py +++ b/pylint/plugins/hass_enforce_greek_micro_char.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from astroid import nodes from pylint.checkers import BaseChecker from pylint.lint import PyLinter @@ -34,13 +36,33 @@ class HassEnforceGreekMicroCharChecker(BaseChecker): self, target: nodes.NodeNG, node: nodes.Assign | nodes.AnnAssign ) -> None: """Check const assignment is not containing ANSI micro char.""" + + def _check_const(node_const: nodes.Const | Any) -> bool: + if ( + isinstance(node_const, nodes.Const) + and isinstance(node_const.value, str) + and "\u00b5" in node_const.value + ): + self.add_message(self.name, node=node) + return True + return False + + # Check constant assignments if ( isinstance(target, nodes.AssignName) and isinstance(node.value, nodes.Const) - and isinstance(node.value.value, str) - and "\u00b5" in node.value.value + and _check_const(node.value) ): - self.add_message(self.name, node=node) + return + + # Check dict with EntityDescription calls + if isinstance(target, nodes.AssignName) and isinstance(node.value, nodes.Dict): + for _, subnode in node.value.items: + if not isinstance(subnode, nodes.Call): + continue + for keyword in subnode.keywords: + if _check_const(keyword.value): + return def register(linter: PyLinter) -> None: diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 5ead5735dcf..d0ea1eb321a 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3755,6 +3755,34 @@ async def test_compile_hourly_statistics_convert_units_1( 30, ), (None, "m3", "m³", None, "volume", 13.050847, 13.333333, -10, 30), + (None, "\u00b5V", "\u03bcV", None, "voltage", 13.050847, 13.333333, -10, 30), + (None, "\u00b5Sv/h", "\u03bcSv/h", None, None, 13.050847, 13.333333, -10, 30), + ( + None, + "\u00b5S/cm", + "\u03bcS/cm", + None, + "conductivity", + 13.050847, + 13.333333, + -10, + 30, + ), + (None, "\u00b5g/ft³", "\u03bcg/ft³", None, None, 13.050847, 13.333333, -10, 30), + (None, "\u00b5g/m³", "\u03bcg/m³", None, None, 13.050847, 13.333333, -10, 30), + ( + None, + "\u00b5mol/s⋅m²", + "\u03bcmol/s⋅m²", + None, + None, + 13.050847, + 13.333333, + -10, + 30, + ), + (None, "\u00b5g", "\u03bcg", None, "mass", 13.050847, 13.333333, -10, 30), + (None, "\u00b5s", "\u03bcs", None, "duration", 13.050847, 13.333333, -10, 30), ], ) async def test_compile_hourly_statistics_equivalent_units_1( @@ -5716,6 +5744,14 @@ async def test_validate_statistics_unit_change_no_conversion( (NONE_SENSOR_ATTRIBUTES, "m3", "m³"), (NONE_SENSOR_ATTRIBUTES, "rpm", "RPM"), (NONE_SENSOR_ATTRIBUTES, "RPM", "rpm"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5V", "\u03bcV"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5Sv/h", "\u03bcSv/h"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5S/cm", "\u03bcS/cm"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5g/ft³", "\u03bcg/ft³"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5g/m³", "\u03bcg/m³"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5mol/s⋅m²", "\u03bcmol/s⋅m²"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5g", "\u03bcg"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5s", "\u03bcs"), ], ) async def test_validate_statistics_unit_change_equivalent_units( @@ -5779,6 +5815,14 @@ async def test_validate_statistics_unit_change_equivalent_units( ("attributes", "unit1", "unit2", "supported_unit"), [ (NONE_SENSOR_ATTRIBUTES, "m³", "m3", "CCF, L, fl. oz., ft³, gal, mL, m³"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcV", "\u00b5V", "\u03bcV"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcSv/h", "\u00b5Sv/h", "\u03bcSv/h"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcS/cm", "\u00b5S/cm", "\u03bcS/cm"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcg/ft³", "\u00b5g/ft³", "\u03bcg/ft³"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcg/m³", "\u00b5g/m³", "\u03bcg/m³"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcmol/s⋅m²", "\u00b5mol/s⋅m²", "\u03bcmol/s⋅m²"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcg", "\u00b5g", "\u03bcg"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcs", "\u00b5s", "\u03bcs"), ], ) async def test_validate_statistics_unit_change_equivalent_units_2( diff --git a/tests/pylint/test_enforce_greek_micro_char.py b/tests/pylint/test_enforce_greek_micro_char.py index 22741542c5d..1412ed32df9 100644 --- a/tests/pylint/test_enforce_greek_micro_char.py +++ b/tests/pylint/test_enforce_greek_micro_char.py @@ -45,6 +45,25 @@ from . import assert_no_messages """, id="good_str_enum", ), + pytest.param( + """ + SENSOR_DESCRIPTION = { + "radiation_rate": AranetSensorEntityDescription( + key="radiation_rate", + translation_key="radiation_rate", + name="Radiation Dose Rate", + native_unit_of_measurement="μSv/h", # "μ" == "\u03bc" + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + scale=0.001, + ), + } + OTHER_DICT = { + "value_with_bad_mu_should_pass": "µ" + } + """, + id="good_sensor_description", + ), ], ) def test_enforce_greek_micro_char( @@ -95,6 +114,22 @@ def test_enforce_greek_micro_char( """, id="bad_str_enum", ), + pytest.param( + """ + SENSOR_DESCRIPTION = { + "radiation_rate": AranetSensorEntityDescription( + key="radiation_rate", + translation_key="radiation_rate", + name="Radiation Dose Rate", + native_unit_of_measurement="µSv/h", # "μ" != "\u03bc" + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + scale=0.001, + ), + } + """, + id="bad_sensor_description", + ), ], ) def test_enforce_greek_micro_char_assign_bad(