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

1"""Bouncie Sensors.""" 

2from __future__ import annotations 

3 

4from collections.abc import Callable 

5from dataclasses import dataclass 

6 

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 

18 

19from . import BouncieDataUpdateCoordinator, const, patch_missing_data 

20 

21import json 

22 

23ATTRIBUTION = "Data provided by Bouncie" 

24PARALLEL_UPDATES = 1 

25 

26 

27@dataclass 

28class BouncieSensorEntityDescriptionMixin: 

29 """Mixing for Bouncie entity.""" 

30 

31 value_fn: Callable 

32 extra_attrs_fn: Callable 

33 

34 

35@dataclass 

36class BouncieSensorEntityDescription( 

37 SensorEntityDescription, BouncieSensorEntityDescriptionMixin 

38): 

39 """Entity description class for Bouncie sensors.""" 

40 

41 

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 } 

47 

48 

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)} 

56 

57 

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 } 

64 

65 

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) 

150 

151 

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 ) 

167 

168 

169class BouncieSensor(CoordinatorEntity[BouncieDataUpdateCoordinator], SensorEntity): 

170 """Bouncie sensor.""" 

171 

172 _attr_attribution = ATTRIBUTION 

173 entity_description: BouncieSensorEntityDescription 

174 

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) 

194 

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() 

205 

206 @property 

207 def native_value(self) -> str | None: 

208 """Return state value.""" 

209 return self.entity_description.value_fn(self._vehicle_info) 

210 

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)