Enable BMW component to be unit system aware (#17197)
* Enable BMW component to be unit system aware * lint fixes * use constants for config entries * remove configuration from component and rely only on HA config of unit_system * remove unused import * update code to reflect feedback * lint fixes * remove unnecessary comments * rework return statement to satisfy pylint * more lint fixes * add tests for volume utils * lint fixes * more lint fixes * remove unnecessary comments
This commit is contained in:
parent
58af332d21
commit
cffb704311
6 changed files with 158 additions and 20 deletions
|
@ -8,6 +8,7 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||||
|
from homeassistant.const import LENGTH_KILOMETERS
|
||||||
|
|
||||||
DEPENDENCIES = ['bmw_connected_drive']
|
DEPENDENCIES = ['bmw_connected_drive']
|
||||||
|
|
||||||
|
@ -117,7 +118,8 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||||
result['lights_parking'] = vehicle_state.parking_lights.value
|
result['lights_parking'] = vehicle_state.parking_lights.value
|
||||||
elif self._attribute == 'condition_based_services':
|
elif self._attribute == 'condition_based_services':
|
||||||
for report in vehicle_state.condition_based_services:
|
for report in vehicle_state.condition_based_services:
|
||||||
result.update(self._format_cbs_report(report))
|
result.update(
|
||||||
|
self._format_cbs_report(report))
|
||||||
elif self._attribute == 'check_control_messages':
|
elif self._attribute == 'check_control_messages':
|
||||||
check_control_messages = vehicle_state.check_control_messages
|
check_control_messages = vehicle_state.check_control_messages
|
||||||
if not check_control_messages:
|
if not check_control_messages:
|
||||||
|
@ -175,8 +177,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||||
'CONNECTED')
|
'CONNECTED')
|
||||||
|
|
||||||
@staticmethod
|
def _format_cbs_report(self, report):
|
||||||
def _format_cbs_report(report):
|
|
||||||
result = {}
|
result = {}
|
||||||
service_type = report.service_type.lower().replace('_', ' ')
|
service_type = report.service_type.lower().replace('_', ' ')
|
||||||
result['{} status'.format(service_type)] = report.state.value
|
result['{} status'.format(service_type)] = report.state.value
|
||||||
|
@ -184,8 +185,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||||
result['{} date'.format(service_type)] = \
|
result['{} date'.format(service_type)] = \
|
||||||
report.due_date.strftime('%Y-%m-%d')
|
report.due_date.strftime('%Y-%m-%d')
|
||||||
if report.due_distance is not None:
|
if report.due_distance is not None:
|
||||||
result['{} distance'.format(service_type)] = \
|
distance = round(self.hass.config.units.length(
|
||||||
'{} km'.format(report.due_distance)
|
report.due_distance, LENGTH_KILOMETERS))
|
||||||
|
result['{} distance'.format(service_type)] = '{} {}'.format(
|
||||||
|
distance, self.hass.config.units.length_unit)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def update_callback(self):
|
def update_callback(self):
|
||||||
|
|
|
@ -9,7 +9,7 @@ import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD)
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -85,6 +85,7 @@ def setup_account(account_config: dict, hass, name: str) \
|
||||||
password = account_config[CONF_PASSWORD]
|
password = account_config[CONF_PASSWORD]
|
||||||
region = account_config[CONF_REGION]
|
region = account_config[CONF_REGION]
|
||||||
read_only = account_config[CONF_READ_ONLY]
|
read_only = account_config[CONF_READ_ONLY]
|
||||||
|
|
||||||
_LOGGER.debug('Adding new account %s', name)
|
_LOGGER.debug('Adding new account %s', name)
|
||||||
cd_account = BMWConnectedDriveAccount(username, password, region, name,
|
cd_account = BMWConnectedDriveAccount(username, password, region, name,
|
||||||
read_only)
|
read_only)
|
||||||
|
|
|
@ -9,18 +9,32 @@ import logging
|
||||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.icon import icon_for_battery_level
|
from homeassistant.helpers.icon import icon_for_battery_level
|
||||||
|
from homeassistant.const import (CONF_UNIT_SYSTEM_IMPERIAL, VOLUME_LITERS,
|
||||||
|
VOLUME_GALLONS, LENGTH_KILOMETERS,
|
||||||
|
LENGTH_MILES)
|
||||||
|
|
||||||
DEPENDENCIES = ['bmw_connected_drive']
|
DEPENDENCIES = ['bmw_connected_drive']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_TO_HA = {
|
ATTR_TO_HA_METRIC = {
|
||||||
'mileage': ['mdi:speedometer', 'km'],
|
'mileage': ['mdi:speedometer', LENGTH_KILOMETERS],
|
||||||
'remaining_range_total': ['mdi:ruler', 'km'],
|
'remaining_range_total': ['mdi:ruler', LENGTH_KILOMETERS],
|
||||||
'remaining_range_electric': ['mdi:ruler', 'km'],
|
'remaining_range_electric': ['mdi:ruler', LENGTH_KILOMETERS],
|
||||||
'remaining_range_fuel': ['mdi:ruler', 'km'],
|
'remaining_range_fuel': ['mdi:ruler', LENGTH_KILOMETERS],
|
||||||
'max_range_electric': ['mdi:ruler', 'km'],
|
'max_range_electric': ['mdi:ruler', LENGTH_KILOMETERS],
|
||||||
'remaining_fuel': ['mdi:gas-station', 'l'],
|
'remaining_fuel': ['mdi:gas-station', VOLUME_LITERS],
|
||||||
|
'charging_time_remaining': ['mdi:update', 'h'],
|
||||||
|
'charging_status': ['mdi:battery-charging', None]
|
||||||
|
}
|
||||||
|
|
||||||
|
ATTR_TO_HA_IMPERIAL = {
|
||||||
|
'mileage': ['mdi:speedometer', LENGTH_MILES],
|
||||||
|
'remaining_range_total': ['mdi:ruler', LENGTH_MILES],
|
||||||
|
'remaining_range_electric': ['mdi:ruler', LENGTH_MILES],
|
||||||
|
'remaining_range_fuel': ['mdi:ruler', LENGTH_MILES],
|
||||||
|
'max_range_electric': ['mdi:ruler', LENGTH_MILES],
|
||||||
|
'remaining_fuel': ['mdi:gas-station', VOLUME_GALLONS],
|
||||||
'charging_time_remaining': ['mdi:update', 'h'],
|
'charging_time_remaining': ['mdi:update', 'h'],
|
||||||
'charging_status': ['mdi:battery-charging', None]
|
'charging_status': ['mdi:battery-charging', None]
|
||||||
}
|
}
|
||||||
|
@ -28,6 +42,11 @@ ATTR_TO_HA = {
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the BMW sensors."""
|
"""Set up the BMW sensors."""
|
||||||
|
if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
|
||||||
|
attribute_info = ATTR_TO_HA_IMPERIAL
|
||||||
|
else:
|
||||||
|
attribute_info = ATTR_TO_HA_METRIC
|
||||||
|
|
||||||
accounts = hass.data[BMW_DOMAIN]
|
accounts = hass.data[BMW_DOMAIN]
|
||||||
_LOGGER.debug('Found BMW accounts: %s',
|
_LOGGER.debug('Found BMW accounts: %s',
|
||||||
', '.join([a.name for a in accounts]))
|
', '.join([a.name for a in accounts]))
|
||||||
|
@ -36,9 +55,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
for vehicle in account.account.vehicles:
|
for vehicle in account.account.vehicles:
|
||||||
for attribute_name in vehicle.drive_train_attributes:
|
for attribute_name in vehicle.drive_train_attributes:
|
||||||
device = BMWConnectedDriveSensor(account, vehicle,
|
device = BMWConnectedDriveSensor(account, vehicle,
|
||||||
attribute_name)
|
attribute_name,
|
||||||
|
attribute_info)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
device = BMWConnectedDriveSensor(account, vehicle, 'mileage')
|
device = BMWConnectedDriveSensor(account, vehicle, 'mileage',
|
||||||
|
attribute_info)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
add_entities(devices, True)
|
add_entities(devices, True)
|
||||||
|
|
||||||
|
@ -46,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
class BMWConnectedDriveSensor(Entity):
|
class BMWConnectedDriveSensor(Entity):
|
||||||
"""Representation of a BMW vehicle sensor."""
|
"""Representation of a BMW vehicle sensor."""
|
||||||
|
|
||||||
def __init__(self, account, vehicle, attribute: str):
|
def __init__(self, account, vehicle, attribute: str, attribute_info):
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
self._vehicle = vehicle
|
self._vehicle = vehicle
|
||||||
self._account = account
|
self._account = account
|
||||||
|
@ -54,6 +75,7 @@ class BMWConnectedDriveSensor(Entity):
|
||||||
self._state = None
|
self._state = None
|
||||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||||
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
||||||
|
self._attribute_info = attribute_info
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self) -> bool:
|
def should_poll(self) -> bool:
|
||||||
|
@ -78,14 +100,14 @@ class BMWConnectedDriveSensor(Entity):
|
||||||
"""Icon to use in the frontend, if any."""
|
"""Icon to use in the frontend, if any."""
|
||||||
from bimmer_connected.state import ChargingState
|
from bimmer_connected.state import ChargingState
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
charging_state = vehicle_state.charging_status in \
|
charging_state = vehicle_state.charging_status in [
|
||||||
[ChargingState.CHARGING]
|
ChargingState.CHARGING]
|
||||||
|
|
||||||
if self._attribute == 'charging_level_hv':
|
if self._attribute == 'charging_level_hv':
|
||||||
return icon_for_battery_level(
|
return icon_for_battery_level(
|
||||||
battery_level=vehicle_state.charging_level_hv,
|
battery_level=vehicle_state.charging_level_hv,
|
||||||
charging=charging_state)
|
charging=charging_state)
|
||||||
icon, _ = ATTR_TO_HA.get(self._attribute, [None, None])
|
icon, _ = self._attribute_info.get(self._attribute, [None, None])
|
||||||
return icon
|
return icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -100,7 +122,7 @@ class BMWConnectedDriveSensor(Entity):
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self) -> str:
|
def unit_of_measurement(self) -> str:
|
||||||
"""Get the unit of measurement."""
|
"""Get the unit of measurement."""
|
||||||
_, unit = ATTR_TO_HA.get(self._attribute, [None, None])
|
_, unit = self._attribute_info.get(self._attribute, [None, None])
|
||||||
return unit
|
return unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -116,6 +138,16 @@ class BMWConnectedDriveSensor(Entity):
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
if self._attribute == 'charging_status':
|
if self._attribute == 'charging_status':
|
||||||
self._state = getattr(vehicle_state, self._attribute).value
|
self._state = getattr(vehicle_state, self._attribute).value
|
||||||
|
elif self.unit_of_measurement == VOLUME_GALLONS:
|
||||||
|
value = getattr(vehicle_state, self._attribute)
|
||||||
|
value_converted = self.hass.config.units.volume(value,
|
||||||
|
VOLUME_LITERS)
|
||||||
|
self._state = round(value_converted)
|
||||||
|
elif self.unit_of_measurement == LENGTH_MILES:
|
||||||
|
value = getattr(vehicle_state, self._attribute)
|
||||||
|
value_converted = self.hass.config.units.length(value,
|
||||||
|
LENGTH_KILOMETERS)
|
||||||
|
self._state = round(value_converted)
|
||||||
else:
|
else:
|
||||||
self._state = getattr(vehicle_state, self._attribute)
|
self._state = getattr(vehicle_state, self._attribute)
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||||
TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE)
|
TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE)
|
||||||
from homeassistant.util import temperature as temperature_util
|
from homeassistant.util import temperature as temperature_util
|
||||||
from homeassistant.util import distance as distance_util
|
from homeassistant.util import distance as distance_util
|
||||||
|
from homeassistant.util import volume as volume_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -108,6 +109,13 @@ class UnitSystem:
|
||||||
return distance_util.convert(length, from_unit,
|
return distance_util.convert(length, from_unit,
|
||||||
self.length_unit)
|
self.length_unit)
|
||||||
|
|
||||||
|
def volume(self, volume: Optional[float], from_unit: str) -> float:
|
||||||
|
"""Convert the given volume to this unit system."""
|
||||||
|
if not isinstance(volume, Number):
|
||||||
|
raise TypeError('{} is not a numeric value.'.format(str(volume)))
|
||||||
|
|
||||||
|
return volume_util.convert(volume, from_unit, self.volume_unit)
|
||||||
|
|
||||||
def as_dict(self) -> dict:
|
def as_dict(self) -> dict:
|
||||||
"""Convert the unit system to a dictionary."""
|
"""Convert the unit system to a dictionary."""
|
||||||
return {
|
return {
|
||||||
|
|
45
homeassistant/util/volume.py
Normal file
45
homeassistant/util/volume.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
"""Volume conversion util functions."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from numbers import Number
|
||||||
|
from homeassistant.const import (VOLUME_LITERS, VOLUME_MILLILITERS,
|
||||||
|
VOLUME_GALLONS, VOLUME_FLUID_OUNCE,
|
||||||
|
VOLUME, UNIT_NOT_RECOGNIZED_TEMPLATE)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
VALID_UNITS = [VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS,
|
||||||
|
VOLUME_FLUID_OUNCE]
|
||||||
|
|
||||||
|
|
||||||
|
def __liter_to_gallon(liter: float) -> float:
|
||||||
|
"""Convert a volume measurement in Liter to Gallon."""
|
||||||
|
return liter * .2642
|
||||||
|
|
||||||
|
|
||||||
|
def __gallon_to_liter(gallon: float) -> float:
|
||||||
|
"""Convert a volume measurement in Gallon to Liter."""
|
||||||
|
return gallon * 3.785
|
||||||
|
|
||||||
|
|
||||||
|
def convert(volume: float, from_unit: str, to_unit: str) -> float:
|
||||||
|
"""Convert a temperature from one unit to another."""
|
||||||
|
if from_unit not in VALID_UNITS:
|
||||||
|
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit,
|
||||||
|
VOLUME))
|
||||||
|
if to_unit not in VALID_UNITS:
|
||||||
|
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, VOLUME))
|
||||||
|
|
||||||
|
if not isinstance(volume, Number):
|
||||||
|
raise TypeError('{} is not of numeric type'.format(volume))
|
||||||
|
|
||||||
|
if from_unit == to_unit:
|
||||||
|
return volume
|
||||||
|
|
||||||
|
result = volume
|
||||||
|
if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS:
|
||||||
|
result = __liter_to_gallon(volume)
|
||||||
|
elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS:
|
||||||
|
result = __gallon_to_liter(volume)
|
||||||
|
|
||||||
|
return result
|
49
tests/util/test_volume.py
Normal file
49
tests/util/test_volume.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
"""Test homeassistant volume utility functions."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import homeassistant.util.volume as volume_util
|
||||||
|
from homeassistant.const import (VOLUME_LITERS, VOLUME_MILLILITERS,
|
||||||
|
VOLUME_GALLONS, VOLUME_FLUID_OUNCE)
|
||||||
|
|
||||||
|
INVALID_SYMBOL = 'bob'
|
||||||
|
VALID_SYMBOL = VOLUME_LITERS
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolumeUtil(unittest.TestCase):
|
||||||
|
"""Test the volume utility functions."""
|
||||||
|
|
||||||
|
def test_convert_same_unit(self):
|
||||||
|
"""Test conversion from any unit to same unit."""
|
||||||
|
self.assertEqual(2, volume_util.convert(2, VOLUME_LITERS,
|
||||||
|
VOLUME_LITERS))
|
||||||
|
self.assertEqual(3, volume_util.convert(3, VOLUME_MILLILITERS,
|
||||||
|
VOLUME_MILLILITERS))
|
||||||
|
self.assertEqual(4, volume_util.convert(4, VOLUME_GALLONS,
|
||||||
|
VOLUME_GALLONS))
|
||||||
|
self.assertEqual(5, volume_util.convert(5, VOLUME_FLUID_OUNCE,
|
||||||
|
VOLUME_FLUID_OUNCE))
|
||||||
|
|
||||||
|
def test_convert_invalid_unit(self):
|
||||||
|
"""Test exception is thrown for invalid units."""
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
volume_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
volume_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL)
|
||||||
|
|
||||||
|
def test_convert_nonnumeric_value(self):
|
||||||
|
"""Test exception is thrown for nonnumeric type."""
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
volume_util.convert('a', VOLUME_GALLONS, VOLUME_LITERS)
|
||||||
|
|
||||||
|
def test_convert_from_liters(self):
|
||||||
|
"""Test conversion from liters to other units."""
|
||||||
|
liters = 5
|
||||||
|
self.assertEqual(volume_util.convert(liters, VOLUME_LITERS,
|
||||||
|
VOLUME_GALLONS), 1.321)
|
||||||
|
|
||||||
|
def test_convert_from_gallons(self):
|
||||||
|
"""Test conversion from gallons to other units."""
|
||||||
|
gallons = 5
|
||||||
|
self.assertEqual(volume_util.convert(gallons, VOLUME_GALLONS,
|
||||||
|
VOLUME_LITERS), 18.925)
|
Loading…
Add table
Add a link
Reference in a new issue