Coverage for custom_components/bouncie/sensor.py: 100%
56 statements
« prev ^ index » next coverage.py v7.2.4, created at 2025-01-04 23:39 +0000
« prev ^ index » next coverage.py v7.2.4, created at 2025-01-04 23:39 +0000
1"""Bouncie Sensors."""
2from __future__ import annotations
4from collections.abc import Callable
5from dataclasses import dataclass
7from homeassistant.components.sensor import (
8 SensorDeviceClass,
9 SensorEntity,
10 SensorEntityDescription,
11 SensorStateClass,
12)
13from homeassistant.config_entries import ConfigEntry
14from homeassistant.core import HomeAssistant, callback
15from homeassistant.helpers.entity import DeviceInfo
16from homeassistant.helpers.entity_platform import AddEntitiesCallback
17from homeassistant.helpers.update_coordinator import CoordinatorEntity
19from . import BouncieDataUpdateCoordinator, const, patch_missing_data
21import json
23ATTRIBUTION = "Data provided by Bouncie"
24PARALLEL_UPDATES = 1
27@dataclass
28class BouncieSensorEntityDescriptionMixin:
29 """Mixing for Bouncie entity."""
31 value_fn: Callable
32 extra_attrs_fn: Callable
35@dataclass
36class BouncieSensorEntityDescription(
37 SensorEntityDescription, BouncieSensorEntityDescriptionMixin
38):
39 """Entity description class for Bouncie sensors."""
42def update_car_stats_attributes(vehicle_info):
43 """Return car statistics update time."""
44 return {
45 const.ATTR_VEHICLE_STATS_LAST_UPDATED_KEY: vehicle_info["stats"]["lastUpdated"]
46 }
49def update_car_info_attributes(vehicle_info):
50 """Return car information."""
51 extra_attrs = {}
52 extra_attrs[const.ATTR_VEHICLE_STANDARD_ENGINE_KEY] = vehicle_info["standardEngine"]
53 extra_attrs[const.ATTR_VEHICLE_VIN_KEY] = vehicle_info["vin"]
54 extra_attrs[const.ATTR_VEHICLE_IMEI_KEY] = vehicle_info["imei"]
55 return {**extra_attrs, **update_car_stats_attributes(vehicle_info)}
58def update_dtc_info_attributes(vehicle_info):
59 """Return DTC details"""
60 return {
61 const.ATTR_VEHICLE_MIL_LAST_UPDATED_KEY: vehicle_info["stats"]["mil"]["lastUpdated"],
62 const.ATTR_VEHICLE_DTC_CODES: vehicle_info["stats"]["mil"]["dtcDetails"]
63 }
66SENSORS: tuple[BouncieSensorEntityDescription, ...] = (
67 BouncieSensorEntityDescription(
68 key="car-last-update",
69 icon="mdi:code-json",
70 name="Last Update",
71 value_fn=lambda vehicle_info: vehicle_info["stats"]["lastUpdated"],
72 extra_attrs_fn=lambda vehicle_info: {
73 const.ATTR_VEHICLE_STATS_LAST_UPDATE_JSON_KEY: json.dumps(vehicle_info)
74 },
75 ),
76 BouncieSensorEntityDescription(
77 key="car-info",
78 icon="mdi:car",
79 name="Car Info",
80 value_fn=lambda vehicle_info: "Running"
81 if vehicle_info["stats"]["isRunning"]
82 else "Not Running",
83 extra_attrs_fn=update_car_info_attributes,
84 ),
85 BouncieSensorEntityDescription(
86 key="car-odometer",
87 icon="mdi:counter",
88 name="Car Odometer",
89 native_unit_of_measurement="mi",
90 state_class=SensorStateClass.TOTAL_INCREASING,
91 value_fn=lambda vehicle_info: int(vehicle_info["stats"]["odometer"]),
92 extra_attrs_fn=update_car_stats_attributes,
93 ),
94 BouncieSensorEntityDescription(
95 key="car-address",
96 icon="mdi:map-marker",
97 name="Car Address",
98 value_fn=lambda vehicle_info: vehicle_info["stats"]["location"]["address"],
99 extra_attrs_fn=update_car_stats_attributes,
100 ),
101 BouncieSensorEntityDescription(
102 key="car-fuel",
103 icon="mdi:gas-station",
104 name="Car Fuel",
105 device_class=SensorDeviceClass.VOLUME_STORAGE,
106 native_unit_of_measurement="%",
107 value_fn=lambda vehicle_info: int(vehicle_info["stats"]["fuelLevel"]),
108 extra_attrs_fn=update_car_stats_attributes,
109 ),
110 BouncieSensorEntityDescription(
111 key="car-speed",
112 icon="mdi:speedometer",
113 name="Car Speed",
114 state_class=SensorStateClass.MEASUREMENT,
115 device_class=SensorDeviceClass.SPEED,
116 suggested_unit_of_measurement="mph",
117 native_unit_of_measurement="mph",
118 value_fn=lambda vehicle_info: int(vehicle_info["stats"]["speed"]),
119 extra_attrs_fn=update_car_stats_attributes,
120 ),
121 BouncieSensorEntityDescription(
122 key="car-mil",
123 icon="mdi:engine",
124 name="Car MIL",
125 value_fn=lambda vehicle_info: vehicle_info["stats"]["mil"]["milOn"],
126 extra_attrs_fn=lambda vehicle_info: {
127 const.ATTR_VEHICLE_MIL_LAST_UPDATED_KEY: vehicle_info["stats"]["mil"][
128 "lastUpdated"]
129 },
130 ),
131 BouncieSensorEntityDescription(
132 key="car-dtc-count",
133 icon="mdi:engine",
134 name="Car DTC Count",
135 state_class=SensorStateClass.TOTAL,
136 value_fn=lambda vehicle_info: int(vehicle_info["stats"]["mil"]["dtcCount"]),
137 extra_attrs_fn=update_dtc_info_attributes,
138 ),
139 BouncieSensorEntityDescription(
140 key="car-battery",
141 icon="mdi:car-battery",
142 name="Car Battery",
143 value_fn=lambda vehicle_info: vehicle_info["stats"]["battery"]["status"],
144 extra_attrs_fn=lambda vehicle_info: {
145 const.ATTR_VEHICLE_BATTERY_LAST_UPDATED_KEY: vehicle_info["stats"][
146 "battery"]["lastUpdated"]
147 },
148 ),
149)
152async def async_setup_entry(
153 hass: HomeAssistant,
154 config_entry: ConfigEntry,
155 async_add_entities: AddEntitiesCallback,
156) -> None:
157 """Set up Bouncie sensor entities based on a config entry."""
158 coordinator = hass.data[const.DOMAIN][config_entry.entry_id]
159 for vehicle_info in coordinator.data["vehicles"]:
160 async_add_entities(
161 [
162 BouncieSensor(coordinator, sensor, dict(vehicle_info))
163 for sensor in SENSORS
164 ],
165 True,
166 )
169class BouncieSensor(CoordinatorEntity[BouncieDataUpdateCoordinator], SensorEntity):
170 """Bouncie sensor."""
172 _attr_attribution = ATTRIBUTION
173 entity_description: BouncieSensorEntityDescription
175 def __init__(
176 self,
177 coordinator: BouncieDataUpdateCoordinator,
178 description: BouncieSensorEntityDescription,
179 vehicle_info: dict,
180 ) -> None:
181 """Init the BouncieSensor."""
182 self._vehicle_info = patch_missing_data(vehicle_info)
183 self.entity_description = description
184 self._attr_has_entity_name = True
185 self._attr_unique_id = self._vehicle_info["vin"] + self.entity_description.key
186 self._attr_device_info = DeviceInfo(
187 identifiers={(const.DOMAIN, self._vehicle_info["vin"])},
188 manufacturer=self._vehicle_info[const.VEHICLE_MODEL_KEY]["make"],
189 model=self._vehicle_info[const.VEHICLE_MODEL_KEY]["name"],
190 name=self._vehicle_info["nickName"],
191 hw_version=self._vehicle_info[const.VEHICLE_MODEL_KEY]["year"],
192 )
193 super().__init__(coordinator)
195 @callback
196 def _handle_coordinator_update(self) -> None:
197 self._vehicle_info = [
198 vehicle
199 for vehicle in self.coordinator.data["vehicles"]
200 if vehicle["vin"] == self._vehicle_info["vin"]
201 ][0] or self._vehicle_info
202 self._vehicle_info = patch_missing_data(self._vehicle_info)
203 self.async_write_ha_state()
204 return super()._handle_coordinator_update()
206 @property
207 def native_value(self) -> str | None:
208 """Return state value."""
209 return self.entity_description.value_fn(self._vehicle_info)
211 @property
212 def extra_state_attributes(self) -> dict[str, str]:
213 """Return state attributes."""
214 return self.entity_description.extra_attrs_fn(self._vehicle_info)