From a51b5cdba9f9d2bc34c8c8102fa4db454976bfc9 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:30:21 +0000 Subject: [PATCH 01/13] Add an option to set energy consumption unit (HAC-179) --- custom_components/volvo_cars/config_flow.py | 100 ++++++++++++++++---- custom_components/volvo_cars/const.py | 5 + custom_components/volvo_cars/sensor.py | 41 +++++++- custom_components/volvo_cars/strings.json | 9 +- tests/test_sensor.py | 48 ++++++++++ 5 files changed, 179 insertions(+), 24 deletions(-) diff --git a/custom_components/volvo_cars/config_flow.py b/custom_components/volvo_cars/config_flow.py index bed8052..2110af8 100644 --- a/custom_components/volvo_cars/config_flow.py +++ b/custom_components/volvo_cars/config_flow.py @@ -27,6 +27,7 @@ SelectSelector, SelectSelectorConfig, ) +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .const import ( CONF_OTP, @@ -35,9 +36,12 @@ DOMAIN, MANUFACTURER, OPT_DEVICE_TRACKER_PICTURE, + OPT_ENERGY_CONSUMPTION_UNIT, OPT_FUEL_CONSUMPTION_UNIT, OPT_IMG_BG_COLOR, OPT_IMG_TRANSPARENT, + OPT_UNIT_ENERGY_KWH_PER_100KM, + OPT_UNIT_ENERGY_MILES_PER_KWH, OPT_UNIT_LITER_PER_100KM, OPT_UNIT_MPG_UK, OPT_UNIT_MPG_US, @@ -247,11 +251,39 @@ async def _async_create_or_update_entry(self) -> ConfigFlowResult: data_updates=data, ) + def _default_energy_unit() -> str: + return ( + OPT_UNIT_ENERGY_MILES_PER_KWH + if ( + self.hass.config.units == US_CUSTOMARY_SYSTEM + or self.hass.config.country in ("UK", "US") + ) + else OPT_UNIT_ENERGY_KWH_PER_100KM + ) + + def _default_fuel_unit() -> str: + if self.hass.config.country == "UK": + return OPT_UNIT_MPG_UK + + if ( + self.hass.config.units == US_CUSTOMARY_SYSTEM + or self.hass.config.country == "US" + ): + return OPT_UNIT_MPG_US + + return OPT_UNIT_LITER_PER_100KM + _LOGGER.debug("Creating entry") return self.async_create_entry( title=f"{MANUFACTURER} {self._vin}", data=data, - options={OPT_FUEL_CONSUMPTION_UNIT: OPT_UNIT_LITER_PER_100KM}, + options={ + CONF_VCC_API_KEY: self._api_key, + OPT_ENERGY_CONSUMPTION_UNIT: _default_energy_unit(), + OPT_FUEL_CONSUMPTION_UNIT: _default_fuel_unit(), + OPT_IMG_BG_COLOR: [0, 0, 0], + OPT_IMG_TRANSPARENT: True, + }, ) @@ -304,26 +336,53 @@ async def async_step_init( } # Units + unit_schema: dict[vol.Marker, Any] = {} + + if coordinator.vehicle.has_battery_engine(): + unit_schema.update( + { + vol.Required( + OPT_ENERGY_CONSUMPTION_UNIT, + default=get_setting( + self.config_entry, OPT_ENERGY_CONSUMPTION_UNIT + ), + ): SelectSelector( + SelectSelectorConfig( + options=[ + OPT_UNIT_ENERGY_KWH_PER_100KM, + OPT_UNIT_ENERGY_MILES_PER_KWH, + ], + multiple=False, + translation_key=OPT_ENERGY_CONSUMPTION_UNIT, + ) + ) + } + ) + if coordinator.vehicle.has_combustion_engine(): - schema.update( - _create_section( - "units", - { - vol.Required(OPT_FUEL_CONSUMPTION_UNIT): SelectSelector( - SelectSelectorConfig( - options=[ - OPT_UNIT_LITER_PER_100KM, - OPT_UNIT_MPG_UK, - OPT_UNIT_MPG_US, - ], - multiple=False, - translation_key=OPT_FUEL_CONSUMPTION_UNIT, - ) + unit_schema.update( + { + vol.Required( + OPT_FUEL_CONSUMPTION_UNIT, + default=get_setting( + self.config_entry, OPT_FUEL_CONSUMPTION_UNIT + ), + ): SelectSelector( + SelectSelectorConfig( + options=[ + OPT_UNIT_LITER_PER_100KM, + OPT_UNIT_MPG_UK, + OPT_UNIT_MPG_US, + ], + multiple=False, + translation_key=OPT_FUEL_CONSUMPTION_UNIT, ) - }, - ) + ) + } ) + schema.update(_create_section("units", unit_schema)) + # Images url = coordinator.vehicle.images.exterior_image_url url_parts = parse.urlparse(url) @@ -333,10 +392,13 @@ async def async_step_init( _create_section( "images", { - vol.Optional(OPT_IMG_TRANSPARENT, default=True): bool, + vol.Optional( + OPT_IMG_TRANSPARENT, + default=get_setting(self.config_entry, OPT_IMG_TRANSPARENT), + ): bool, vol.Optional( OPT_IMG_BG_COLOR, - default=[255, 255, 255], + default=get_setting(self.config_entry, OPT_IMG_BG_COLOR), ): ColorRGBSelector(), }, ) diff --git a/custom_components/volvo_cars/const.py b/custom_components/volvo_cars/const.py index f7634a4..76aee7d 100644 --- a/custom_components/volvo_cars/const.py +++ b/custom_components/volvo_cars/const.py @@ -28,14 +28,19 @@ MANUFACTURER = "Volvo" OPT_DEVICE_TRACKER_PICTURE = "device_tracker_picture" +OPT_ENERGY_CONSUMPTION_UNIT = "energy_consumption_unit" OPT_FUEL_CONSUMPTION_UNIT = "fuel_consumption_unit" OPT_IMG_BG_COLOR = "image_bg_color" OPT_IMG_TRANSPARENT = "image_transparent" +OPT_UNIT_ENERGY_KWH_PER_100KM = "kwh_100km" +OPT_UNIT_ENERGY_MILES_PER_KWH = "miles_kwh" + OPT_UNIT_LITER_PER_100KM = "l_100km" OPT_UNIT_MPG_UK = "mpg_uk" OPT_UNIT_MPG_US = "mpg_us" + SERVICE_REFRESH_DATA = "refresh_data" SERVICE_PARAM_DATA = "data" SERVICE_PARAM_ENTRY = "entry" diff --git a/custom_components/volvo_cars/sensor.py b/custom_components/volvo_cars/sensor.py index a3b0f2f..f23f882 100644 --- a/custom_components/volvo_cars/sensor.py +++ b/custom_components/volvo_cars/sensor.py @@ -23,7 +23,10 @@ from .const import ( DATA_BATTERY_CAPACITY, DATA_REQUEST_COUNT, + OPT_ENERGY_CONSUMPTION_UNIT, OPT_FUEL_CONSUMPTION_UNIT, + OPT_UNIT_ENERGY_KWH_PER_100KM, + OPT_UNIT_ENERGY_MILES_PER_KWH, OPT_UNIT_LITER_PER_100KM, OPT_UNIT_MPG_UK, OPT_UNIT_MPG_US, @@ -46,8 +49,8 @@ class VolvoCarsSensorDescription(VolvoCarsDescription, SensorEntityDescription): """Describes a Volvo Cars sensor entity.""" value_fn: Callable[[VolvoCarsValue, VolvoCarsConfigEntry], Any] | None = None - available_fn: Callable[[VolvoCarsVehicle], bool] = lambda vehicle: True unit_fn: Callable[[VolvoCarsConfigEntry], str] | None = None + available_fn: Callable[[VolvoCarsVehicle], bool] = lambda vehicle: True def _availability_status(field: VolvoCarsValue, _: VolvoCarsConfigEntry) -> str: @@ -65,6 +68,30 @@ def _calculate_time_to_service(field: VolvoCarsValue, _: VolvoCarsConfigEntry) - return value +def _determine_energy_consumption_unit(entry: VolvoCarsConfigEntry) -> str: + unit_key = entry.options.get( + OPT_ENERGY_CONSUMPTION_UNIT, OPT_UNIT_ENERGY_KWH_PER_100KM + ) + + return "kWh/100 km" if unit_key == OPT_UNIT_ENERGY_KWH_PER_100KM else "mi/kWh" + + +def _convert_energy_consumption( + field: VolvoCarsValue, entry: VolvoCarsConfigEntry +) -> Decimal: + value = Decimal(field.value) + unit_key = entry.options.get( + OPT_ENERGY_CONSUMPTION_UNIT, OPT_UNIT_ENERGY_KWH_PER_100KM + ) + + converted_value = value + + if unit_key == OPT_UNIT_ENERGY_MILES_PER_KWH: + converted_value = (100 / Decimal(1.609344) / value) if value else Decimal(0) + + return round(converted_value, 1) + + def _determine_fuel_consumption_unit(entry: VolvoCarsConfigEntry) -> str: unit_key = entry.options.get(OPT_FUEL_CONSUMPTION_UNIT, OPT_UNIT_LITER_PER_100KM) @@ -130,28 +157,34 @@ def _convert_fuel_consumption( key="average_energy_consumption", translation_key="average_energy_consumption", api_field="averageEnergyConsumption", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement="kWh/100 km", device_class=SensorDeviceClass.ENERGY, icon="mdi:car-electric", available_fn=lambda vehicle: vehicle.has_battery_engine(), + unit_fn=_determine_energy_consumption_unit, + value_fn=_convert_energy_consumption, ), VolvoCarsSensorDescription( key="average_energy_consumption_automatic", translation_key="average_energy_consumption_automatic", api_field="averageEnergyConsumptionAutomatic", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement="kWh/100 km", device_class=SensorDeviceClass.ENERGY, icon="mdi:car-electric", available_fn=lambda vehicle: vehicle.has_battery_engine(), + unit_fn=_determine_energy_consumption_unit, + value_fn=_convert_energy_consumption, ), VolvoCarsSensorDescription( key="average_energy_consumption_charge", translation_key="average_energy_consumption_charge", api_field="averageEnergyConsumptionSinceCharge", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement="kWh/100 km", device_class=SensorDeviceClass.ENERGY, icon="mdi:car-electric", available_fn=lambda vehicle: vehicle.has_battery_engine(), + unit_fn=_determine_energy_consumption_unit, + value_fn=_convert_energy_consumption, ), VolvoCarsSensorDescription( key="average_fuel_consumption", diff --git a/custom_components/volvo_cars/strings.json b/custom_components/volvo_cars/strings.json index 6d0c8ee..ba69a35 100644 --- a/custom_components/volvo_cars/strings.json +++ b/custom_components/volvo_cars/strings.json @@ -1196,7 +1196,8 @@ }, "units": { "data": { - "fuel_consumption_unit": "Fuel consumption unit" + "fuel_consumption_unit": "Fuel consumption unit", + "energy_consumption_unit": "Energy consumption unit" }, "name": "Units" } @@ -1205,6 +1206,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/tests/test_sensor.py b/tests/test_sensor.py index fbf51ab..4263839 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -6,7 +6,10 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.volvo_cars.const import ( + OPT_ENERGY_CONSUMPTION_UNIT, OPT_FUEL_CONSUMPTION_UNIT, + OPT_UNIT_ENERGY_KWH_PER_100KM, + OPT_UNIT_ENERGY_MILES_PER_KWH, OPT_UNIT_LITER_PER_100KM, OPT_UNIT_MPG_UK, OPT_UNIT_MPG_US, @@ -99,3 +102,48 @@ async def test_fuel_unit_conversion( assert entity assert entity.state == value assert entity.attributes.get("unit_of_measurement") == unit_of_measurement + + +@pytest.mark.parametrize( + ("entity_id", "unit", "value", "unit_of_measurement"), + [ + ( + "sensor.volvo_myvolvo_average_energy_consumption", + OPT_UNIT_ENERGY_KWH_PER_100KM, + "22.6", + "kWh/100 km", + ), + ( + "sensor.volvo_myvolvo_average_energy_consumption", + OPT_UNIT_ENERGY_MILES_PER_KWH, + "2.7", + "mi/kWh", + ), + ], +) +@pytest.mark.use_model("xc40_bev") +async def test_energy_unit_conversion( + hass: HomeAssistant, + enable_custom_integrations: None, + mock_config_entry: MockConfigEntry, + entity_id: str, + unit: str, + value: str, + unit_of_measurement: str, +) -> None: + """Test energy unit conversion.""" + + with patch("custom_components.volvo_cars.PLATFORMS", [Platform.SENSOR]): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + hass.config_entries.async_update_entry( + mock_config_entry, + options={OPT_ENERGY_CONSUMPTION_UNIT: unit}, + ) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity + assert entity.state == value + assert entity.attributes.get("unit_of_measurement") == unit_of_measurement From 8d3db5ac505afdd5c098cb5317f42108a99490b6 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:31:25 +0000 Subject: [PATCH 02/13] =?UTF-8?q?Use=20proper=20lang=20code=20for=20Norweg?= =?UTF-8?q?ian=20Bokm=C3=A5l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/volvo_cars/translations/{no.json => nb.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename custom_components/volvo_cars/translations/{no.json => nb.json} (100%) diff --git a/custom_components/volvo_cars/translations/no.json b/custom_components/volvo_cars/translations/nb.json similarity index 100% rename from custom_components/volvo_cars/translations/no.json rename to custom_components/volvo_cars/translations/nb.json From aa20b9393a27a323deed998df38bc849f4266abf Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:23:01 +0000 Subject: [PATCH 03/13] Add sanity checks for VIN number (#83) --- custom_components/volvo_cars/config_flow.py | 32 ++++++++++++++------- custom_components/volvo_cars/strings.json | 3 +- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/custom_components/volvo_cars/config_flow.py b/custom_components/volvo_cars/config_flow.py index 2110af8..11b49af 100644 --- a/custom_components/volvo_cars/config_flow.py +++ b/custom_components/volvo_cars/config_flow.py @@ -4,6 +4,7 @@ from collections.abc import Mapping import logging +import re from typing import TYPE_CHECKING, Any, Self from urllib import parse @@ -52,6 +53,7 @@ from .volvo.models import AuthorizationModel, VolvoAuthException _LOGGER = logging.getLogger(__name__) +_VIN_REGEX = re.compile(r"[A-Z0-9]{17}") def get_setting(entry: VolvoCarsConfigEntry, key: str) -> Any: @@ -95,21 +97,31 @@ async def async_step_user( errors: dict[str, str] = {} if user_input is not None: - flow = await self._async_authenticate( - user_input[CONF_VIN], user_input, errors - ) + vin = str(user_input[CONF_VIN]).strip().upper() - if flow is not None: - return flow + if _VIN_REGEX.fullmatch(vin) is None: + errors[CONF_VIN] = "invalid_vin" + else: + flow = await self._async_authenticate(vin, user_input, errors) + + if flow is not None: + return flow + user_input = user_input or {} schema = vol.Schema( { - vol.Required(CONF_USERNAME, default=self._username or ""): str, - vol.Required(CONF_PASSWORD, default=self._password or ""): str, - vol.Required(CONF_VIN, default=self._vin or ""): str, - vol.Required(CONF_VCC_API_KEY, default=self._api_key or ""): str, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + vol.Required(CONF_VIN, default=user_input.get(CONF_VIN, "")): str, + vol.Required( + CONF_VCC_API_KEY, default=user_input.get(CONF_VCC_API_KEY, "") + ): str, vol.Optional( - CONF_FRIENDLY_NAME, default=self._friendly_name or "" + CONF_FRIENDLY_NAME, default=user_input.get(CONF_FRIENDLY_NAME, "") ): str, }, ) diff --git a/custom_components/volvo_cars/strings.json b/custom_components/volvo_cars/strings.json index ba69a35..5fb77bd 100644 --- a/custom_components/volvo_cars/strings.json +++ b/custom_components/volvo_cars/strings.json @@ -31,7 +31,8 @@ "reauth_successful": "Successfully updated credentials" }, "error": { - "invalid_auth": "Authentication failed" + "invalid_auth": "Authentication failed", + "invalid_vin": "VIN number is invalid" }, "step": { "otp": { From e668520105df774ad63fad020d1ad81cbb1b6164 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:20:45 +0000 Subject: [PATCH 04/13] Allow empty device tracker picture --- custom_components/volvo_cars/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/volvo_cars/config_flow.py b/custom_components/volvo_cars/config_flow.py index 11b49af..5c58322 100644 --- a/custom_components/volvo_cars/config_flow.py +++ b/custom_components/volvo_cars/config_flow.py @@ -341,7 +341,8 @@ async def async_step_init( OPT_DEVICE_TRACKER_PICTURE, default=get_setting( self.config_entry, OPT_DEVICE_TRACKER_PICTURE - ), + ) + or vol.UNDEFINED, ): EntitySelector(EntitySelectorConfig(domain=Platform.IMAGE)) }, ), From e2106738058ad3c85a78de3640ca700eda38a134 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:43:04 +0000 Subject: [PATCH 05/13] Keep API key in sync --- custom_components/volvo_cars/config_flow.py | 42 ++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/custom_components/volvo_cars/config_flow.py b/custom_components/volvo_cars/config_flow.py index 5c58322..c1e25d0 100644 --- a/custom_components/volvo_cars/config_flow.py +++ b/custom_components/volvo_cars/config_flow.py @@ -47,7 +47,7 @@ OPT_UNIT_MPG_UK, OPT_UNIT_MPG_US, ) -from .coordinator import VolvoCarsConfigEntry, VolvoCarsData +from .coordinator import VolvoCarsData from .factory import async_create_auth_api from .store import VolvoCarsStoreManager from .volvo.models import AuthorizationModel, VolvoAuthException @@ -56,7 +56,7 @@ _VIN_REGEX = re.compile(r"[A-Z0-9]{17}") -def get_setting(entry: VolvoCarsConfigEntry, key: str) -> Any: +def get_setting(entry: ConfigEntry, key: str) -> Any: """Get setting from options with a fallback to config.""" if key in entry.options: return entry.options[key] @@ -165,12 +165,15 @@ async def async_step_reauth_confirm( reauth_entry = self._get_reauth_entry() if user_input is not None: - flow = await self._async_authenticate( - reauth_entry.data[CONF_VIN], user_input, errors - ) + vin = str(reauth_entry.data[CONF_VIN]).strip().upper() + + if _VIN_REGEX.fullmatch(vin) is None: + errors[CONF_VIN] = "invalid_vin" + else: + flow = await self._async_authenticate(vin, user_input, errors) - if flow is not None: - return flow + if flow is not None: + return flow schema = vol.Schema( { @@ -179,7 +182,8 @@ async def async_step_reauth_confirm( ): str, vol.Required(CONF_PASSWORD, default=""): str, vol.Required( - CONF_VCC_API_KEY, default=reauth_entry.data.get(CONF_VCC_API_KEY) + CONF_VCC_API_KEY, + default=get_setting(reauth_entry, CONF_VCC_API_KEY), ): str, }, ) @@ -239,13 +243,6 @@ async def _async_authenticate( return None async def _async_create_or_update_entry(self) -> ConfigFlowResult: - data = { - CONF_USERNAME: self._username, - CONF_VIN: self._vin, - CONF_VCC_API_KEY: self._api_key, - CONF_FRIENDLY_NAME: self._friendly_name, - } - if self._auth_result and self._auth_result.token: if self.unique_id is None: raise ConfigEntryError("Config entry has no unique_id") @@ -257,10 +254,21 @@ async def _async_create_or_update_entry(self) -> ConfigFlowResult: refresh_token=self._auth_result.token.refresh_token, ) + data = { + CONF_USERNAME: self._username, + CONF_VIN: self._vin, + CONF_VCC_API_KEY: self._api_key, + CONF_FRIENDLY_NAME: self._friendly_name, + } + if self.source == SOURCE_REAUTH: + # Keep API key in sync with options + reauth_entry = self._get_reauth_entry() + options = dict(reauth_entry.options) if reauth_entry else {} + options.update({CONF_VCC_API_KEY: self._api_key}) + return self.async_update_reload_and_abort( - self._get_reauth_entry(), - data_updates=data, + self._get_reauth_entry(), data_updates=data, options=options ) def _default_energy_unit() -> str: From ed21c8443220ac481b820ce2b5cea8e9ae1f4053 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:47:12 +0000 Subject: [PATCH 06/13] Set state class for average consumption --- custom_components/volvo_cars/sensor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/volvo_cars/sensor.py b/custom_components/volvo_cars/sensor.py index f23f882..9d897e5 100644 --- a/custom_components/volvo_cars/sensor.py +++ b/custom_components/volvo_cars/sensor.py @@ -158,7 +158,7 @@ def _convert_fuel_consumption( translation_key="average_energy_consumption", api_field="averageEnergyConsumption", native_unit_of_measurement="kWh/100 km", - device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:car-electric", available_fn=lambda vehicle: vehicle.has_battery_engine(), unit_fn=_determine_energy_consumption_unit, @@ -169,7 +169,7 @@ def _convert_fuel_consumption( translation_key="average_energy_consumption_automatic", api_field="averageEnergyConsumptionAutomatic", native_unit_of_measurement="kWh/100 km", - device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:car-electric", available_fn=lambda vehicle: vehicle.has_battery_engine(), unit_fn=_determine_energy_consumption_unit, @@ -180,7 +180,7 @@ def _convert_fuel_consumption( translation_key="average_energy_consumption_charge", api_field="averageEnergyConsumptionSinceCharge", native_unit_of_measurement="kWh/100 km", - device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:car-electric", available_fn=lambda vehicle: vehicle.has_battery_engine(), unit_fn=_determine_energy_consumption_unit, @@ -191,6 +191,7 @@ def _convert_fuel_consumption( translation_key="average_fuel_consumption", api_field="averageFuelConsumption", native_unit_of_measurement="L/100 km", + state_class=SensorStateClass.MEASUREMENT, icon="mdi:gas-station", available_fn=lambda vehicle: vehicle.has_combustion_engine(), unit_fn=_determine_fuel_consumption_unit, @@ -201,6 +202,7 @@ def _convert_fuel_consumption( translation_key="average_fuel_consumption_automatic", api_field="averageFuelConsumptionAutomatic", native_unit_of_measurement="L/100 km", + state_class=SensorStateClass.MEASUREMENT, icon="mdi:gas-station", available_fn=lambda vehicle: vehicle.has_combustion_engine(), unit_fn=_determine_fuel_consumption_unit, From 7a2efba9ad83816fc0d3c305a37b04b389de5424 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:50:40 +0000 Subject: [PATCH 07/13] Grammar update --- custom_components/volvo_cars/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/volvo_cars/strings.json b/custom_components/volvo_cars/strings.json index 5fb77bd..40fd96a 100644 --- a/custom_components/volvo_cars/strings.json +++ b/custom_components/volvo_cars/strings.json @@ -32,7 +32,7 @@ }, "error": { "invalid_auth": "Authentication failed", - "invalid_vin": "VIN number is invalid" + "invalid_vin": "VIN is invalid" }, "step": { "otp": { From 28ddb4fc1edd390945302ef3f13984e175514e23 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:10:35 +0000 Subject: [PATCH 08/13] Update readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11fd340..331a1a5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Now check out the features section for details on what you'll get! - Multiple cars - Multiple accounts -- Multiple languages: Dutch (nl), English (en), German (de), Norwegian (no), Polish (pl), Portuguese (pt & pt-BR), Swedish (sv) +- Multiple languages: Dutch (nl), English (en), French (fr), German (de), Norwegian Bokmål (nb), Polish (pl), Portuguese Brazil (pt-BR), Portuguese Portugal (pt), Swedish (sv)
@@ -165,7 +165,8 @@ Once a car has been added, you can configure additional options for it. | ----------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------- | | Volvo API key | The generated API key in the developer account. | | | Device tracker picture | The picture that will be shown on the map. | | -| Fuel consumption unit | You can choose between `l/100km`, `mpg (UK)` and `mpg (US)`. | Cars with a combustion engine. | +| Energy consumption unit | You can choose between `kWh/100 km` and `mi/kWh`. | Cars with a battery engine. | +| Fuel consumption unit | You can choose between `l/100 km`, `mpg (UK)` and `mpg (US)` | Cars with a combustion engine. | | Images transparent background | Whether or not you want transparent a background for the exterior images. | Depending on the image URL provided by the Volvo API. | | Images background color | Choose the background color for the exterior images. | Depending on the image URL provided by the Volvo API. | From 6af376d25005234471b14da548c769285dab3998 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:56:24 +0000 Subject: [PATCH 09/13] Add publish action --- .github/workflows/publish.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e69de29 From 47b0aa49b53131ef442c8587b3423f75273b50a9 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:08:47 +0000 Subject: [PATCH 10/13] Add publish action --- .github/workflows/publish.yml | 40 ++++++++++++++++++++++ custom_components/volvo_cars/manifest.json | 2 +- hacs.json | 4 ++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e69de29..ad7214b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: Publish + +on: + release: + types: + - published + workflow_dispatch: + +jobs: + release: + name: Publish zip file + runs-on: ubuntu-latest + permissions: + contents: write + env: + INTEGRATION_PATH: ${{ github.workspace }}/custom_components/volvo_cars + steps: + - name: Check out code + uses: actions/checkout@v4.2.2 + + - name: Set version in manifest.json + run: | + # Extract version from tag (e.g., "refs/tags/v1.2.3" becomes "1.2.3") + version="${GITHUB_REF#refs/tags/v}" + echo "Setting version to ${version}" + # Update the "version" key in manifest.json (assumes a simple "version": "..." line) + sed -i -E "s/\"version\": \"[^\"]+\"/\"version\": \"${version}\"/" ${INTEGRATION_PATH}/manifest.json + + - name: Zip + run: | + cd $INTEGRATION_PATH + zip volvo_cars.zip -r ./ + pwd + ls -l *.zip + + - name: Include zip in release + uses: softprops/action-gh-release@v2.2.1 + with: + files: ${{ env.INTEGRATION_PATH }}/volvo_cars.zip + fail_on_unmatched_files: true diff --git a/custom_components/volvo_cars/manifest.json b/custom_components/volvo_cars/manifest.json index 40668aa..e17b273 100644 --- a/custom_components/volvo_cars/manifest.json +++ b/custom_components/volvo_cars/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/thomasddn/ha-volvo-cars/issues", "requirements": [], - "version": "1.3.0" + "version": "0.0.0" } \ No newline at end of file diff --git a/hacs.json b/hacs.json index a0b8a53..25ae754 100644 --- a/hacs.json +++ b/hacs.json @@ -2,5 +2,7 @@ "name": "Volvo Cars", "homeassistant": "2024.11.0", "hide_default_branch": true, - "render_readme": true + "render_readme": true, + "zip_release": true, + "filename": "volvo_cars.zip" } \ No newline at end of file From 609e75983d462a8267da899a7f133b390e093884 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:59:55 +0100 Subject: [PATCH 11/13] Lokalise: updates --- .../volvo_cars/translations/de.json | 10 +- .../volvo_cars/translations/en.json | 10 +- .../volvo_cars/translations/fr.json | 1258 +++++++++++++++++ .../volvo_cars/translations/nb.json | 12 +- .../volvo_cars/translations/nl.json | 10 +- .../volvo_cars/translations/pl.json | 10 +- .../volvo_cars/translations/pt-BR.json | 10 +- .../volvo_cars/translations/pt.json | 10 +- .../volvo_cars/translations/sv.json | 24 +- 9 files changed, 1338 insertions(+), 16 deletions(-) create mode 100644 custom_components/volvo_cars/translations/fr.json diff --git a/custom_components/volvo_cars/translations/de.json b/custom_components/volvo_cars/translations/de.json index 0af4a75..ec4f226 100644 --- a/custom_components/volvo_cars/translations/de.json +++ b/custom_components/volvo_cars/translations/de.json @@ -31,7 +31,8 @@ "reauth_successful": "Zugangsdaten erfolgreich aktualisiert" }, "error": { - "invalid_auth": "Authentifizierung fehlgeschlagen" + "invalid_auth": "Authentifizierung fehlgeschlagen", + "invalid_vin": "Fahrzeug-Identifikationsnummer ist ungültig" }, "step": { "otp": { @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "Einheit Energieverbrauch", "fuel_consumption_unit": "Verbrauchseinheit" }, "name": "Einheit" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/custom_components/volvo_cars/translations/en.json b/custom_components/volvo_cars/translations/en.json index 96c7266..aa1456f 100644 --- a/custom_components/volvo_cars/translations/en.json +++ b/custom_components/volvo_cars/translations/en.json @@ -31,7 +31,8 @@ "reauth_successful": "Successfully updated credentials" }, "error": { - "invalid_auth": "Authentication failed" + "invalid_auth": "Authentication failed", + "invalid_vin": "VIN is invalid" }, "step": { "otp": { @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "Energy consumption unit", "fuel_consumption_unit": "Fuel consumption unit" }, "name": "Units" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/custom_components/volvo_cars/translations/fr.json b/custom_components/volvo_cars/translations/fr.json new file mode 100644 index 0000000..28af978 --- /dev/null +++ b/custom_components/volvo_cars/translations/fr.json @@ -0,0 +1,1258 @@ +{ + "common": { + "api_timestamp": "Horodatage API", + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "door_open": "Portière ouverte", + "error": "Erreur", + "high_pressure": "Pression élevée", + "last_refresh": "Dernière actualisation", + "last_result": "Dernier résultat", + "low_pressure": "Pression faible", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "ok": "OK", + "pressure": "Pression", + "rejected": "Rejeté", + "running": "Allumé", + "time_frame_passed": "Délai écoulé", + "timeout": "Expiré", + "unknown": "Inconnu", + "vcc_api_key": "Clé d'API Volvo", + "vehicle_in_sleep": "Véhicule en veille", + "very_low_pressure": "Pression très faible", + "waiting": "En attente", + "wrong_usage": "Mauvaise utilisation" + }, + "config": { + "abort": { + "already_configured": "La voiture est déjà configurée", + "reauth_successful": "Mise à jour des informations d'identification réussie" + }, + "error": { + "invalid_auth": "Échec de l'authentification", + "invalid_vin": "Le numéro VIN est incorrect" + }, + "step": { + "otp": { + "data": { + "otp": "OTP" + }, + "description": "Saisissez l'OTP que vous avez reçu dans votre boîte mail." + }, + "reauth_confirm": { + "description": "L'intégration de Volvo Cars doit ré-authentifier votre compte.", + "title": "Informations d’identification expirées" + }, + "user": { + "data": { + "friendly_name": "Nom d'affichage", + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "vcc_api_key": "Clé d'API Volvo", + "vin": "Numéro d'identification du véhicule" + }, + "data_description": { + "friendly_name": "Cette valeur est utilisée dans l'ID d'entité (volvo_[nom d'affichage]_[clé]). Si elle est laissée vide, le VIN sera utilisé.", + "vin": "Le VIN comporte 17 caractères et commence probablement par YV1 ou YV4." + }, + "description": "Saisissez les informations d'identification de votre compte Volvo, le numéro VIN et la clé API de développeur Volvo (https://developer.volvocars.com/)." + } + } + }, + "entity": { + "binary_sensor": { + "brake_fluid_level_warning": { + "name": "Liquide de frein", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "brake_light_center_warning": { + "name": "Feu stop central", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "brake_light_left_warning": { + "name": "Feu stop gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "brake_light_right_warning": { + "name": "Feu stop droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "coolant_level_warning": { + "name": "Niveau du liquide de refroidissement", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "daytime_running_light_left_warning": { + "name": "Feu de jour gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "daytime_running_light_right_warning": { + "name": "Feu de jour droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "door_front_left": { + "name": "Portière avant gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "door_front_right": { + "name": "Portière avant droite", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "door_rear_left": { + "name": "Portière arrière gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "door_rear_right": { + "name": "Portière arrière droite", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "engine_status": { + "name": "État du moteur", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "fog_light_front_warning": { + "name": "Feu antibrouillard avant", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "fog_light_rear_warning": { + "name": "Feu antibrouillard arrière", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "hazard_lights_warning": { + "name": "Feux de détresse", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "high_beam_left_warning": { + "name": "Feu de route gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "high_beam_right_warning": { + "name": "Feu de route droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "hood": { + "name": "Capot", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "low_beam_left_warning": { + "name": "Feu de croisement gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "low_beam_right_warning": { + "name": "Feu de croisement droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "oil_level_warning": { + "name": "Niveau d'huile", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "level": { + "name": "Niveau", + "state": { + "no_warning": "OK", + "service_required": "Entretien nécessaire", + "too_high": "Trop élevé", + "too_low": "Trop faible" + } + } + } + }, + "position_light_front_left_warning": { + "name": "Feu de position avant gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "position_light_front_right_warning": { + "name": "Feu de position avant droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "position_light_rear_left_warning": { + "name": "Feu de position arrière gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "position_light_rear_right_warning": { + "name": "Feu de position arrière droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "registration_plate_light_warning": { + "name": "Éclairage plaque d'immatriculation", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "reverse_lights_warning": { + "name": "Feux de recul", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "service_warning": { + "name": "Entretien", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "reason": { + "name": "Motif", + "state": { + "distance_driven_almost_time_for_service": "Entretien périodique à prévoir", + "distance_driven_overdue_for_service": "Entretien périodique en retard", + "distance_driven_time_for_service": "Entretien périodique à effectuer", + "engine_hours_almost_time_for_service": "Entretien moteur à prévoir", + "engine_hours_overdue_for_service": "Entretien moteur en retard", + "engine_hours_time_for_service": "Entretien moteur à effectuer", + "no_warning": "OK", + "regular_maintenance_almost_time_for_service": "Entretien à prévoir", + "regular_maintenance_overdue_for_service": "Entretien en retard", + "regular_maintenance_time_for_service": "Entretien à effectuer", + "unknown_warning": "Avertissement inconnu" + } + } + } + }, + "side_mark_lights_warning": { + "name": "Feux de position latéraux", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "sunroof": { + "name": "Toit ouvrant", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "tailgate": { + "name": "Hayon", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "tank_lid": { + "name": "Trappe à carburant", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "turn_indication_front_left_warning": { + "name": "Clignotant avant gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "turn_indication_front_right_warning": { + "name": "Clignotant avant droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "turn_indication_rear_left_warning": { + "name": "Clignotant arrière gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "turn_indication_rear_right_warning": { + "name": "Clignotant arrière droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "tyre_front_left": { + "name": "Pneu avant gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "pressure": { + "name": "Pression", + "state": { + "high_pressure": "Pression élevée", + "low_pressure": "Pression faible", + "no_warning": "OK", + "very_low_pressure": "Pression très faible" + } + } + } + }, + "tyre_front_right": { + "name": "Pneu avant droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "pressure": { + "name": "Pression", + "state": { + "high_pressure": "Pression élevée", + "low_pressure": "Pression faible", + "no_warning": "OK", + "very_low_pressure": "Pression très faible" + } + } + } + }, + "tyre_rear_left": { + "name": "Pneu arrière gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "pressure": { + "name": "Pression", + "state": { + "high_pressure": "Pression élevée", + "low_pressure": "Pression faible", + "no_warning": "OK", + "very_low_pressure": "Pression très faible" + } + } + } + }, + "tyre_rear_right": { + "name": "Pneu arrière droit", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "pressure": { + "name": "Pression", + "state": { + "high_pressure": "Pression élevée", + "low_pressure": "Pression faible", + "no_warning": "OK", + "very_low_pressure": "Pression très faible" + } + } + } + }, + "washer_fluid_level_warning": { + "name": "Liquide lave-glace", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "window_front_left": { + "name": "Fenêtre avant gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "window_front_right": { + "name": "Fenêtre avant droite", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "window_rear_left": { + "name": "Fenêtre arrière gauche", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "window_rear_right": { + "name": "Fenêtre arrière droite", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + } + }, + "button": { + "climatization_start": { + "name": "Démarrer la climatisation", + "state_attributes": { + "last_result": { + "name": "Dernier résultat", + "state": { + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "running": "Allumé", + "timeout": "Expiré", + "unknown": "Inconnu", + "vehicle_in_sleep": "Véhicule en veille", + "waiting": "En attente" + } + } + } + }, + "climatization_stop": { + "name": "Arrêter la climatisation", + "state_attributes": { + "last_result": { + "name": "Dernier résultat", + "state": { + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "running": "Allumé", + "timeout": "Expiré", + "unknown": "Inconnu", + "vehicle_in_sleep": "Véhicule en veille", + "waiting": "En attente" + } + } + } + }, + "engine_start": { + "name": "Démarrer le moteur", + "state_attributes": { + "last_result": { + "name": "Dernier résultat", + "state": { + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "running": "Allumé", + "timeout": "Expiré", + "unknown": "Inconnu", + "vehicle_in_sleep": "Véhicule en veille", + "waiting": "En attente" + } + } + } + }, + "engine_stop": { + "name": "Arrêter le moteur", + "state_attributes": { + "last_result": { + "name": "Dernier résultat", + "state": { + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "running": "Allumé", + "timeout": "Expiré", + "unknown": "Inconnu", + "vehicle_in_sleep": "Véhicule en veille", + "waiting": "En attente" + } + } + } + }, + "flash": { + "name": "Clignoter", + "state_attributes": { + "last_result": { + "name": "Dernier résultat", + "state": { + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "timeout": "Expiré", + "unknown": "Inconnu", + "vehicle_in_sleep": "Véhicule en veille" + } + } + } + }, + "honk": { + "name": "Avertisseur sonore", + "state_attributes": { + "last_result": { + "name": "Dernier résultat", + "state": { + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "timeout": "Expiré", + "unknown": "Inconnu", + "vehicle_in_sleep": "Véhicule en veille" + } + } + } + }, + "honk_flash": { + "name": "Klaxonner et clignoter", + "state_attributes": { + "last_result": { + "name": "Dernier résultat", + "state": { + "car_error": "Erreur voiture", + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "timeout": "Expiré", + "unknown": "Inconnu", + "vehicle_in_sleep": "Véhicule en veille" + } + } + } + }, + "update_data": { + "name": "Mettre à jour les données" + } + }, + "device_tracker": { + "location": { + "name": "Emplacement", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "direction": { + "name": "Direction" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + } + }, + "image": { + "exterior": { + "name": "Extérieur côté passager" + }, + "exterior_back": { + "name": "Extérieur arrière" + }, + "exterior_back_driver": { + "name": "Extérieur arrière conducteur" + }, + "exterior_back_passenger": { + "name": "Extérieur arrière passager" + }, + "exterior_front": { + "name": "Extérieur avant" + }, + "exterior_front_driver": { + "name": "Extérieur avant conducteur" + }, + "exterior_front_passenger": { + "name": "Extérieur avant passager" + }, + "exterior_side_driver": { + "name": "Extérieur côté conducteur" + }, + "interior": { + "name": "Intérieur" + } + }, + "lock": { + "lock": { + "name": "Verrouillage centralisé", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "last_result": { + "name": "Dernier résultat", + "state": { + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "timeout": "Expiré", + "unable_to_lock_door_open": "Portière ouverte", + "unknown": "Inconnu", + "unlock_time_frame_passed": "Délai écoulé", + "vehicle_in_sleep": "Véhicule en veille" + } + } + } + }, + "lock_reduced_guard": { + "name": "Verrouillage avec alarme réduite", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + }, + "last_result": { + "name": "Dernier résultat", + "state": { + "completed": "Terminé", + "connection_failure": "Échec de la connexion", + "delivered": "Transmis", + "not_allowed_privacy_enabled": "Non autorisé, protection de la vie privée activée", + "not_allowed_wrong_usage_mode": "Mauvaise utilisation", + "rejected": "Rejeté", + "timeout": "Expiré", + "unable_to_lock_door_open": "Portière ouverte", + "unknown": "Inconnu", + "unlock_time_frame_passed": "Délai écoulé", + "vehicle_in_sleep": "Véhicule en veille" + } + } + } + } + }, + "number": { + "data_update_interval": { + "name": "Intervalle de mise à jour des données" + }, + "engine_run_time": { + "name": "Durée de fonctionnement du moteur" + } + }, + "sensor": { + "api_request_count": { + "name": "Nombre de requêtes API" + }, + "api_status": { + "name": "État de l’API", + "state_attributes": { + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "availability": { + "name": "Connexion à la voiture", + "state": { + "available": "Disponible", + "car_in_use": "Voiture en cours d'utilisation", + "no_internet": "Pas d'internet", + "power_saving_mode": "Mode économie d'énergie", + "unavailable": "Indisponible", + "unspecified": "Inconnu" + }, + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "average_energy_consumption": { + "name": "Consommation moyenne d'énergie TM", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "average_energy_consumption_automatic": { + "name": "Consommation moyenne d'énergie TA", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "average_energy_consumption_charge": { + "name": "Consommation énergétique moyenne depuis la charge", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "average_fuel_consumption": { + "name": "Consommation moyenne de carburant TM", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "average_fuel_consumption_automatic": { + "name": "Consommation moyenne de carburant TA", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "average_speed": { + "name": "Vitesse moyenne TM", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "average_speed_automatic": { + "name": "Vitesse moyenne TA", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "battery_capacity": { + "name": "Capacité de la batterie", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "battery_charge_level": { + "name": "Niveau de charge de la batterie", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "charging_connection_status": { + "name": "État de la connexion de charge", + "state": { + "connection_status_connected_ac": "Connecté AC", + "connection_status_connected_dc": "Connecté DC", + "connection_status_disconnected": "Déconnecté", + "connection_status_fault": "Erreur", + "connection_status_unspecified": "Inconnu" + }, + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "charging_system_status": { + "name": "État du chargement", + "state": { + "charging_system_charging": "En charge", + "charging_system_done": "Terminé", + "charging_system_fault": "Erreur", + "charging_system_idle": "Inactif", + "charging_system_scheduled": "Programmé", + "charging_system_unspecified": "Inconnu" + }, + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "distance_to_empty_battery": { + "name": "Autonomie batterie", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "distance_to_empty_tank": { + "name": "Autonomie carburant", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "distance_to_service": { + "name": "Distance jusqu'au prochain entretien", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "engine_time_to_service": { + "name": "Heures d'utilisation jusqu'au prochain entretien", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "estimated_charging_time": { + "name": "Temps de charge estimé", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "fuel_amount": { + "name": "Quantité de carburant", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "odometer": { + "name": "Compteur kilométrique", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "time_to_service": { + "name": "Temps restant jusqu'au prochain entretien", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "trip_meter_automatic": { + "name": "Distance TA", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + }, + "trip_meter_manual": { + "name": "Distance TM", + "state_attributes": { + "api_timestamp": { + "name": "Horodatage API" + }, + "last_refresh": { + "name": "Dernière actualisation" + } + } + } + } + }, + "exceptions": { + "lock_failure": { + "message": "Échec de la commande du véhicule {command}. Statut: {status}. Message: {message}" + } + }, + "options": { + "abort": { + "no_options_available": "Aucune option disponible pour votre véhicule." + }, + "step": { + "init": { + "sections": { + "api": { + "data": { + "vcc_api_key": "Clé d'API Volvo" + }, + "name": "API" + }, + "device_tracker": { + "data": { + "device_tracker_picture": "Image" + }, + "name": "Suivi d'appareil" + }, + "images": { + "data": { + "image_bg_color": "Couleur d'arrière-plan des images", + "image_transparent": "Arrière-plan des images transparent" + }, + "data_description": { + "image_bg_color": "Applicable uniquement si la case \"transparent\" n'est pas cochée." + }, + "name": "Images" + }, + "units": { + "data": { + "energy_consumption_unit": "Unité de consommation d'énergie", + "fuel_consumption_unit": "Unité de consommation de carburant" + }, + "name": "Unités" + } + } + } + } + }, + "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, + "fuel_consumption_unit": { + "options": { + "l_100km": "L/100 km", + "mpg_uk": "mpg (UK)", + "mpg_us": "mpg (US)" + } + }, + "refresh_data_param": { + "options": { + "availability": "Disponibilité", + "brakes": "Freins", + "diagnostics": "Diagnostics", + "doors": "Portières", + "engine": "Moteur", + "engine_status": "État du moteur", + "fuel": "Carburant", + "location": "Localisation", + "odometer": "Compteur kilométrique", + "recharge_status": "État de la recharge", + "statistics": "Statistiques", + "tyres": "Pneus", + "warnings": "Avertissements", + "windows": "Fenêtres" + } + } + }, + "services": { + "refresh_data": { + "description": "Actualiser toutes les données ou uniquement des parties spécifiques.", + "fields": { + "data": { + "description": "Laissez le champ vide pour actualiser toutes les données.", + "name": "Donnée" + }, + "entry": { + "description": "L'entrée à rafraîchir. Laissez vide pour actualiser les données de toutes les entrées.", + "name": "Entrée" + } + }, + "name": "Actualiser les données" + } + } +} \ No newline at end of file diff --git a/custom_components/volvo_cars/translations/nb.json b/custom_components/volvo_cars/translations/nb.json index d98c123..f26a461 100644 --- a/custom_components/volvo_cars/translations/nb.json +++ b/custom_components/volvo_cars/translations/nb.json @@ -31,7 +31,8 @@ "reauth_successful": "Oppdatering av autentiseringsinformasjon vellykket." }, "error": { - "invalid_auth": "Oppdatering av autentiseringsinformasjon misslyktes." + "invalid_auth": "Oppdatering av autentiseringsinformasjon misslyktes.", + "invalid_vin": "Ikke gyldig understellsnummer" }, "step": { "otp": { @@ -1035,7 +1036,7 @@ "charging_system_charging": "Lader", "charging_system_done": "Ferdig", "charging_system_fault": "Feil", - "charging_system_idle": "Tomgang", + "charging_system_idle": "Inaktiv", "charging_system_scheduled": "Planlagt", "charging_system_unspecified": "Ukjent" }, @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "Enhet for strømforbruk", "fuel_consumption_unit": "Drivstofforbruk enhet" }, "name": "Enheter" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/custom_components/volvo_cars/translations/nl.json b/custom_components/volvo_cars/translations/nl.json index 38218fc..93c9718 100644 --- a/custom_components/volvo_cars/translations/nl.json +++ b/custom_components/volvo_cars/translations/nl.json @@ -31,7 +31,8 @@ "reauth_successful": "Inloggegevens succesvol bijgewerkt" }, "error": { - "invalid_auth": "Aanmelden mislukt" + "invalid_auth": "Aanmelden mislukt", + "invalid_vin": "Ongeldige VIN" }, "step": { "otp": { @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "Eenheid energieverbruik", "fuel_consumption_unit": "Eenheid brandstofverbruik" }, "name": "Eenheden" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/custom_components/volvo_cars/translations/pl.json b/custom_components/volvo_cars/translations/pl.json index 9f6a629..2f49ee4 100644 --- a/custom_components/volvo_cars/translations/pl.json +++ b/custom_components/volvo_cars/translations/pl.json @@ -31,7 +31,8 @@ "reauth_successful": "Dane uwierzytelniające zostały zaktualizowane" }, "error": { - "invalid_auth": "Autentykacja nie powiodła się" + "invalid_auth": "Autentykacja nie powiodła się", + "invalid_vin": "VIN jest nieprawidłowy" }, "step": { "otp": { @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "Jednostka zużycia energii", "fuel_consumption_unit": "Jednostka zużycia paliwa" }, "name": "Jednostki" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/custom_components/volvo_cars/translations/pt-BR.json b/custom_components/volvo_cars/translations/pt-BR.json index c93ba61..98da225 100644 --- a/custom_components/volvo_cars/translations/pt-BR.json +++ b/custom_components/volvo_cars/translations/pt-BR.json @@ -31,7 +31,8 @@ "reauth_successful": "Credenciais atualizadas com sucesso" }, "error": { - "invalid_auth": "Falha na autenticação" + "invalid_auth": "Falha na autenticação", + "invalid_vin": "" }, "step": { "otp": { @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "", "fuel_consumption_unit": "Unidade de consumo de combustível" }, "name": "Unidades" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/custom_components/volvo_cars/translations/pt.json b/custom_components/volvo_cars/translations/pt.json index 7cc0efc..50d282e 100644 --- a/custom_components/volvo_cars/translations/pt.json +++ b/custom_components/volvo_cars/translations/pt.json @@ -31,7 +31,8 @@ "reauth_successful": "Credenciais atualizadas com êxito" }, "error": { - "invalid_auth": "Falha na autenticação" + "invalid_auth": "Falha na autenticação", + "invalid_vin": "Número VIN inválido" }, "step": { "otp": { @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "Unidade de consumo de energia", "fuel_consumption_unit": "Unidade de consumo de combustível" }, "name": "Unidades" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", diff --git a/custom_components/volvo_cars/translations/sv.json b/custom_components/volvo_cars/translations/sv.json index 7192764..d10756e 100644 --- a/custom_components/volvo_cars/translations/sv.json +++ b/custom_components/volvo_cars/translations/sv.json @@ -15,7 +15,7 @@ "ok": "OK", "pressure": "Tryck", "rejected": "Avvisad", - "running": "Löper", + "running": "Kör", "time_frame_passed": "Tidsram passerad", "timeout": "Timeout", "unknown": "Okänd", @@ -31,7 +31,8 @@ "reauth_successful": "Autentiseringsuppgifterna har uppdaterats" }, "error": { - "invalid_auth": "Autentiseringen misslyckades" + "invalid_auth": "Autentiseringen misslyckades", + "invalid_vin": "VIN-numret är felaktigt" }, "step": { "otp": { @@ -633,7 +634,7 @@ "not_allowed_privacy_enabled": "Inte tillåtet, sekretess aktiverad", "not_allowed_wrong_usage_mode": "Fel användning", "rejected": "Avvisad", - "running": "Löper", + "running": "Kör", "timeout": "Timeout", "unknown": "Okänd", "vehicle_in_sleep": "Fordonet i dvala", @@ -655,7 +656,7 @@ "not_allowed_privacy_enabled": "Inte tillåtet, sekretess aktiverad", "not_allowed_wrong_usage_mode": "Fel användning", "rejected": "Avvisad", - "running": "Löper", + "running": "Kör", "timeout": "Timeout", "unknown": "Okänd", "vehicle_in_sleep": "Fordonet i dvala", @@ -677,7 +678,7 @@ "not_allowed_privacy_enabled": "Inte tillåtet, sekretess aktiverad", "not_allowed_wrong_usage_mode": "Fel användning", "rejected": "Avvisad", - "running": "Löper", + "running": "Kör", "timeout": "Timeout", "unknown": "Okänd", "vehicle_in_sleep": "Fordonet i dvala", @@ -699,7 +700,7 @@ "not_allowed_privacy_enabled": "Inte tillåtet, sekretess aktiverad", "not_allowed_wrong_usage_mode": "Fel användning", "rejected": "Avvisad", - "running": "Löper", + "running": "Kör", "timeout": "Timeout", "unknown": "Okänd", "vehicle_in_sleep": "Fordonet i dvala", @@ -1033,9 +1034,9 @@ "name": "Laddningsstatus", "state": { "charging_system_charging": "Laddar", - "charging_system_done": "Utfört", + "charging_system_done": "Laddning klar", "charging_system_fault": "Fel", - "charging_system_idle": "På tomgång", + "charging_system_idle": "Ingen laddning", "charging_system_scheduled": "Schemalagd", "charging_system_unspecified": "Okänd" }, @@ -1196,6 +1197,7 @@ }, "units": { "data": { + "energy_consumption_unit": "Energiförbrukningsenhet", "fuel_consumption_unit": "Bränsleförbrukningsenhet" }, "name": "Enheter" @@ -1205,6 +1207,12 @@ } }, "selector": { + "energy_consumption_unit": { + "options": { + "kwh_100km": "kWh/100 km", + "miles_kwh": "mi/kWh" + } + }, "fuel_consumption_unit": { "options": { "l_100km": "L/100 km", From d879c292ddccc91cc3f074bbeb42a0a50a5b017b Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:33:44 +0000 Subject: [PATCH 12/13] Check translation files for errors --- .github/workflows/ci.yml | 31 +++++++++++++++ scripts/__init__.py | 1 + scripts/check_translations.py | 71 +++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 scripts/__init__.py create mode 100644 scripts/check_translations.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05d42f8..e1f7df9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,37 @@ jobs: pip install -r requirements.txt pip install -r requirements.test.txt + translations: + name: Check translations + runs-on: ubuntu-latest + needs: + - info + - prepare + steps: + - name: Check out code + uses: actions/checkout@v4.2.2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.3.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Restore Python ${{ env.DEFAULT_PYTHON }} virtual environment + id: cache-venv + uses: actions/cache/restore@v4.2.0 + with: + path: venv + fail-on-cache-miss: true + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.python_cache_key }} + + - name: Run translations check + run: | + . venv/bin/activate + python --version + python scripts/check_translations.py --ignore-errors + mypy: name: Check mypy runs-on: ubuntu-latest diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..a9a18d7 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Scripts.""" diff --git a/scripts/check_translations.py b/scripts/check_translations.py new file mode 100644 index 0000000..59f6211 --- /dev/null +++ b/scripts/check_translations.py @@ -0,0 +1,71 @@ +"""Check translation files for errors.""" + +import json +from pathlib import Path +import sys + +BASE_TRANSLATION = Path("custom_components/volvo_cars/strings.json") +TRANSLATIONS_DIR = Path("custom_components/volvo_cars/translations") + + +def _flatten_items(data: dict, parent: str = "") -> dict: + """Return a dict mapping dot-separated keys to their values.""" + items = {} + for key, value in data.items(): + full_key = f"{parent}::{key}" if parent else key + if isinstance(value, dict): + items.update(_flatten_items(value, full_key)) + else: + items[full_key] = value + return items + + +def _load_json(path: Path) -> dict: + with path.open(encoding="utf-8") as f: + return json.load(f) + + +def _is_empty(value) -> bool: + return value is None or (isinstance(value, str) and not value.strip()) + + +def main(): + """Check the translation files for errors.""" + + ignore_errors = "--ignore-errors" in sys.argv + errors = 0 + + base_data = _load_json(BASE_TRANSLATION) + base_items = _flatten_items(base_data) + base_keys = set(base_items.keys()) + + for file in TRANSLATIONS_DIR.glob("*.json"): + print(f"--- {file.name} ---") + + translation_data = _load_json(file) + trans_items = _flatten_items(translation_data) + trans_keys = set(trans_items.keys()) + + missing = base_keys - trans_keys + orphaned = trans_keys - base_keys + empty_values = [k for k, v in trans_items.items() if _is_empty(v)] + + if missing: + print(" Missing keys:", sorted(missing)) + errors = errors + len(missing) + + if orphaned: + print(" Orphaned keys:", sorted(orphaned)) + errors = errors + len(orphaned) + + if empty_values: + print(" Empty values:", sorted(empty_values)) + errors = errors + len(empty_values) + + print() + + sys.exit(0) if ignore_errors else sys.exit(1 if errors > 0 else 0) + + +if __name__ == "__main__": + main() From b17b90c0c668130795a8534aff2f4fe3d16011a0 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:34:51 +0000 Subject: [PATCH 13/13] Update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 331a1a5..aaf4fd1 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Supported regions include: Europe, the Middle East, and Africa. For a complete l Now check out the features section for details on what you'll get! [![GitHub release][releases-shield]][releases] +[![Github downloads][downloads-shield]][releases] [![CI main][ci-main-shield]][ci-workflow] [![CI dev][ci-dev-shield]][ci-workflow] [![HACS][hacs-shield]][hacs] @@ -215,6 +216,7 @@ When I'm coding, I run on coffee, Coca-Cola*, and Lays* potato chips. If you'd l \* No affiliation with these brands — just personal favorites! [releases-shield]: https://img.shields.io/github/v/release/thomasddn/ha-volvo-cars?style=flat-square +[downloads-shield]: https://img.shields.io/github/downloads/thomasddn/ha-volvo-cars/total?style=flat-square [releases]: https://github.com/thomasddn/ha-volvo-cars/releases [ci-dev-shield]: https://img.shields.io/github/actions/workflow/status/thomasddn/ha-volvo-cars/ci.yml?branch=develop&style=flat-square&label=develop [ci-main-shield]: https://img.shields.io/github/actions/workflow/status/thomasddn/ha-volvo-cars/ci.yml?branch=main&style=flat-square&label=main