2019-02-13 20:21:14 +00:00
|
|
|
"""Support for Volvo On Call."""
|
2017-02-19 01:09:25 +00:00
|
|
|
import logging
|
2019-04-04 06:31:55 +00:00
|
|
|
from datetime import timedelta
|
2017-02-19 01:09:25 +00:00
|
|
|
|
2018-02-11 17:20:28 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2017-02-19 01:09:25 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2019-04-04 06:31:55 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_NAME, CONF_PASSWORD, CONF_RESOURCES, CONF_SCAN_INTERVAL, CONF_USERNAME
|
|
|
|
)
|
|
|
|
from homeassistant.helpers import discovery
|
2018-12-13 11:25:40 +00:00
|
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
2018-11-30 18:07:42 +00:00
|
|
|
from homeassistant.helpers.dispatcher import (
|
2019-04-04 06:31:55 +00:00
|
|
|
async_dispatcher_connect, async_dispatcher_send
|
|
|
|
)
|
|
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
2017-02-19 01:09:25 +00:00
|
|
|
from homeassistant.util.dt import utcnow
|
|
|
|
|
|
|
|
DOMAIN = 'volvooncall'
|
|
|
|
|
2017-06-16 05:28:30 +00:00
|
|
|
DATA_KEY = DOMAIN
|
|
|
|
|
2017-02-19 01:09:25 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
MIN_UPDATE_INTERVAL = timedelta(minutes=1)
|
|
|
|
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
|
2017-12-10 21:57:44 +00:00
|
|
|
|
2017-11-11 20:21:25 +00:00
|
|
|
CONF_REGION = 'region'
|
2017-06-07 06:52:36 +00:00
|
|
|
CONF_SERVICE_URL = 'service_url'
|
2017-12-10 21:57:44 +00:00
|
|
|
CONF_SCANDINAVIAN_MILES = 'scandinavian_miles'
|
2018-11-30 18:07:42 +00:00
|
|
|
CONF_MUTABLE = 'mutable'
|
|
|
|
|
|
|
|
SIGNAL_STATE_UPDATED = '{}.updated'.format(DOMAIN)
|
|
|
|
|
|
|
|
COMPONENTS = {
|
|
|
|
'sensor': 'sensor',
|
|
|
|
'binary_sensor': 'binary_sensor',
|
|
|
|
'lock': 'lock',
|
|
|
|
'device_tracker': 'device_tracker',
|
2019-02-13 20:21:14 +00:00
|
|
|
'switch': 'switch',
|
2018-11-30 18:07:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RESOURCES = [
|
|
|
|
'position',
|
|
|
|
'lock',
|
|
|
|
'heater',
|
|
|
|
'odometer',
|
|
|
|
'trip_meter1',
|
|
|
|
'trip_meter2',
|
|
|
|
'fuel_amount',
|
|
|
|
'fuel_amount_level',
|
|
|
|
'average_fuel_consumption',
|
|
|
|
'distance_to_empty',
|
|
|
|
'washer_fluid_level',
|
|
|
|
'brake_fluid',
|
|
|
|
'service_warning_status',
|
|
|
|
'bulb_failures',
|
|
|
|
'battery_range',
|
|
|
|
'battery_level',
|
|
|
|
'time_to_fully_charged',
|
|
|
|
'battery_charge_status',
|
|
|
|
'engine_start',
|
|
|
|
'last_trip',
|
|
|
|
'is_engine_running',
|
2018-12-04 10:39:42 +00:00
|
|
|
'doors_hood_open',
|
|
|
|
'doors_front_left_door_open',
|
|
|
|
'doors_front_right_door_open',
|
|
|
|
'doors_rear_left_door_open',
|
|
|
|
'doors_rear_right_door_open',
|
|
|
|
'windows_front_left_window_open',
|
|
|
|
'windows_front_right_window_open',
|
|
|
|
'windows_rear_left_window_open',
|
|
|
|
'windows_rear_right_window_open',
|
|
|
|
'tyre_pressure_front_left_tyre_pressure',
|
|
|
|
'tyre_pressure_front_right_tyre_pressure',
|
|
|
|
'tyre_pressure_rear_left_tyre_pressure',
|
|
|
|
'tyre_pressure_rear_right_tyre_pressure',
|
2018-11-30 18:07:42 +00:00
|
|
|
'any_door_open',
|
|
|
|
'any_window_open'
|
|
|
|
]
|
2017-02-22 22:39:59 +00:00
|
|
|
|
2017-02-19 01:09:25 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
2019-04-04 06:31:55 +00:00
|
|
|
DOMAIN: vol.Schema({
|
|
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
|
|
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL):
|
|
|
|
vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)),
|
|
|
|
vol.Optional(CONF_NAME, default={}):
|
|
|
|
cv.schema_with_slug_keys(cv.string),
|
|
|
|
vol.Optional(CONF_RESOURCES): vol.All(
|
|
|
|
cv.ensure_list, [vol.In(RESOURCES)]),
|
|
|
|
vol.Optional(CONF_REGION): cv.string,
|
|
|
|
vol.Optional(CONF_SERVICE_URL): cv.string,
|
|
|
|
vol.Optional(CONF_MUTABLE, default=True): cv.boolean,
|
|
|
|
vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean,
|
|
|
|
})
|
2017-02-19 01:09:25 +00:00
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
2018-11-30 18:07:42 +00:00
|
|
|
async def async_setup(hass, config):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up the Volvo On Call component."""
|
2018-12-13 11:25:40 +00:00
|
|
|
session = async_get_clientsession(hass)
|
|
|
|
|
2017-11-11 20:21:25 +00:00
|
|
|
from volvooncall import Connection
|
2017-02-19 01:09:25 +00:00
|
|
|
connection = Connection(
|
2018-12-13 11:25:40 +00:00
|
|
|
session=session,
|
|
|
|
username=config[DOMAIN].get(CONF_USERNAME),
|
|
|
|
password=config[DOMAIN].get(CONF_PASSWORD),
|
|
|
|
service_url=config[DOMAIN].get(CONF_SERVICE_URL),
|
|
|
|
region=config[DOMAIN].get(CONF_REGION))
|
2017-02-19 01:09:25 +00:00
|
|
|
|
2019-02-17 05:23:09 +00:00
|
|
|
interval = config[DOMAIN][CONF_SCAN_INTERVAL]
|
2017-02-19 01:09:25 +00:00
|
|
|
|
2018-11-30 18:07:42 +00:00
|
|
|
data = hass.data[DATA_KEY] = VolvoData(config)
|
2017-02-19 01:09:25 +00:00
|
|
|
|
2018-12-03 18:52:50 +00:00
|
|
|
def is_enabled(attr):
|
|
|
|
"""Return true if the user has enabled the resource."""
|
|
|
|
return attr in config[DOMAIN].get(CONF_RESOURCES, [attr])
|
|
|
|
|
2017-02-19 01:09:25 +00:00
|
|
|
def discover_vehicle(vehicle):
|
|
|
|
"""Load relevant platforms."""
|
2018-11-30 18:07:42 +00:00
|
|
|
data.vehicles.add(vehicle.vin)
|
|
|
|
|
|
|
|
dashboard = vehicle.dashboard(
|
|
|
|
mutable=config[DOMAIN][CONF_MUTABLE],
|
|
|
|
scandinavian_miles=config[DOMAIN][CONF_SCANDINAVIAN_MILES])
|
|
|
|
|
|
|
|
for instrument in (
|
|
|
|
instrument
|
|
|
|
for instrument in dashboard.instruments
|
|
|
|
if instrument.component in COMPONENTS and
|
|
|
|
is_enabled(instrument.slug_attr)):
|
|
|
|
|
2018-12-03 18:52:50 +00:00
|
|
|
data.instruments.add(instrument)
|
2018-11-30 18:07:42 +00:00
|
|
|
|
|
|
|
hass.async_create_task(
|
|
|
|
discovery.async_load_platform(
|
|
|
|
hass,
|
|
|
|
COMPONENTS[instrument.component],
|
|
|
|
DOMAIN,
|
|
|
|
(vehicle.vin,
|
|
|
|
instrument.component,
|
|
|
|
instrument.attr),
|
|
|
|
config))
|
|
|
|
|
|
|
|
async def update(now):
|
2017-02-19 01:09:25 +00:00
|
|
|
"""Update status from the online service."""
|
|
|
|
try:
|
2018-11-30 18:07:42 +00:00
|
|
|
if not await connection.update(journal=True):
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.warning("Could not query server")
|
2017-02-19 01:09:25 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
for vehicle in connection.vehicles:
|
2018-11-30 18:07:42 +00:00
|
|
|
if vehicle.vin not in data.vehicles:
|
|
|
|
discover_vehicle(vehicle)
|
|
|
|
|
|
|
|
async_dispatcher_send(hass, SIGNAL_STATE_UPDATED)
|
2017-02-19 01:09:25 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
finally:
|
2018-11-30 18:07:42 +00:00
|
|
|
async_track_point_in_utc_time(hass, update, utcnow() + interval)
|
2017-02-19 01:09:25 +00:00
|
|
|
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.info("Logging in to service")
|
2018-11-30 18:07:42 +00:00
|
|
|
return await update(utcnow())
|
2017-02-19 01:09:25 +00:00
|
|
|
|
|
|
|
|
2017-09-05 16:10:01 +00:00
|
|
|
class VolvoData:
|
|
|
|
"""Hold component state."""
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
"""Initialize the component state."""
|
2018-11-30 18:07:42 +00:00
|
|
|
self.vehicles = set()
|
2018-12-03 18:52:50 +00:00
|
|
|
self.instruments = set()
|
2017-12-10 21:57:44 +00:00
|
|
|
self.config = config[DOMAIN]
|
|
|
|
self.names = self.config.get(CONF_NAME)
|
2017-09-05 16:10:01 +00:00
|
|
|
|
2018-11-30 18:07:42 +00:00
|
|
|
def instrument(self, vin, component, attr):
|
|
|
|
"""Return corresponding instrument."""
|
|
|
|
return next((instrument
|
|
|
|
for instrument in self.instruments
|
|
|
|
if instrument.vehicle.vin == vin and
|
|
|
|
instrument.component == component and
|
|
|
|
instrument.attr == attr), None)
|
|
|
|
|
2017-09-05 16:10:01 +00:00
|
|
|
def vehicle_name(self, vehicle):
|
|
|
|
"""Provide a friendly name for a vehicle."""
|
|
|
|
if (vehicle.registration_number and
|
|
|
|
vehicle.registration_number.lower()) in self.names:
|
|
|
|
return self.names[vehicle.registration_number.lower()]
|
2018-07-23 08:16:05 +00:00
|
|
|
if vehicle.vin and vehicle.vin.lower() in self.names:
|
2017-09-05 16:10:01 +00:00
|
|
|
return self.names[vehicle.vin.lower()]
|
2018-07-23 08:16:05 +00:00
|
|
|
if vehicle.registration_number:
|
2017-09-05 16:10:01 +00:00
|
|
|
return vehicle.registration_number
|
2018-07-23 08:16:05 +00:00
|
|
|
if vehicle.vin:
|
2017-09-05 16:10:01 +00:00
|
|
|
return vehicle.vin
|
2018-02-11 17:20:28 +00:00
|
|
|
return ''
|
2017-09-05 16:10:01 +00:00
|
|
|
|
|
|
|
|
2017-02-19 01:09:25 +00:00
|
|
|
class VolvoEntity(Entity):
|
|
|
|
"""Base class for all VOC entities."""
|
|
|
|
|
2018-11-30 18:07:42 +00:00
|
|
|
def __init__(self, data, vin, component, attribute):
|
2017-02-19 01:09:25 +00:00
|
|
|
"""Initialize the entity."""
|
2018-11-30 18:07:42 +00:00
|
|
|
self.data = data
|
|
|
|
self.vin = vin
|
|
|
|
self.component = component
|
|
|
|
self.attribute = attribute
|
|
|
|
|
|
|
|
async def async_added_to_hass(self):
|
|
|
|
"""Register update dispatcher."""
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass, SIGNAL_STATE_UPDATED,
|
|
|
|
self.async_schedule_update_ha_state)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def instrument(self):
|
|
|
|
"""Return corresponding instrument."""
|
|
|
|
return self.data.instrument(self.vin, self.component, self.attribute)
|
2017-02-22 22:39:59 +00:00
|
|
|
|
|
|
|
@property
|
2018-11-30 18:07:42 +00:00
|
|
|
def icon(self):
|
|
|
|
"""Return the icon."""
|
|
|
|
return self.instrument.icon
|
2017-02-19 01:09:25 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def vehicle(self):
|
|
|
|
"""Return vehicle."""
|
2018-11-30 18:07:42 +00:00
|
|
|
return self.instrument.vehicle
|
2017-02-19 01:09:25 +00:00
|
|
|
|
2017-02-22 22:39:59 +00:00
|
|
|
@property
|
|
|
|
def _entity_name(self):
|
2018-11-30 18:07:42 +00:00
|
|
|
return self.instrument.name
|
2017-02-19 01:09:25 +00:00
|
|
|
|
2017-09-05 16:10:01 +00:00
|
|
|
@property
|
|
|
|
def _vehicle_name(self):
|
2018-11-30 18:07:42 +00:00
|
|
|
return self.data.vehicle_name(self.vehicle)
|
2017-09-05 16:10:01 +00:00
|
|
|
|
2017-02-19 01:09:25 +00:00
|
|
|
@property
|
2017-02-22 22:39:59 +00:00
|
|
|
def name(self):
|
|
|
|
"""Return full name of the entity."""
|
2017-06-16 05:28:30 +00:00
|
|
|
return '{} {}'.format(
|
2017-02-22 22:39:59 +00:00
|
|
|
self._vehicle_name,
|
|
|
|
self._entity_name)
|
2017-02-19 01:09:25 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the polling state."""
|
2017-02-19 01:09:25 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def assumed_state(self):
|
|
|
|
"""Return true if unable to access real state of entity."""
|
|
|
|
return True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return device specific state attributes."""
|
2018-11-30 18:07:42 +00:00
|
|
|
return dict(self.instrument.attributes,
|
|
|
|
model='{}/{}'.format(
|
|
|
|
self.vehicle.vehicle_type,
|
|
|
|
self.vehicle.model_year))
|