diff --git a/.coveragerc b/.coveragerc index a48355ba652..0dcf1ffa439 100644 --- a/.coveragerc +++ b/.coveragerc @@ -88,6 +88,9 @@ omit = homeassistant/components/verisure.py homeassistant/components/*/verisure.py + homeassistant/components/volvooncall.py + homeassistant/components/*/volvooncall.py + homeassistant/components/*/webostv.py homeassistant/components/wemo.py @@ -183,7 +186,6 @@ omit = homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/ubus.py - homeassistant/components/device_tracker/volvooncall.py homeassistant/components/device_tracker/xiaomi.py homeassistant/components/discovery.py homeassistant/components/downloader.py diff --git a/homeassistant/components/binary_sensor/volvooncall.py b/homeassistant/components/binary_sensor/volvooncall.py new file mode 100644 index 00000000000..0703fd40139 --- /dev/null +++ b/homeassistant/components/binary_sensor/volvooncall.py @@ -0,0 +1,59 @@ +""" +Support for VOC. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.volvooncall/ + +""" +import logging + +from homeassistant.components.volvooncall import VolvoEntity +from homeassistant.components.binary_sensor import BinarySensorDevice + +_LOGGER = logging.getLogger(__name__) + +SENSORS = [('washer_fluid_level', 'Washer fluid'), + ('brake_fluid', 'Brake Fluid'), + ('service_warning_status', 'Service'), + ('bulb_failures', 'Bulbs'), + ('doors', 'Doors'), + ('windows', 'Windows')] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Volvo sensors.""" + if discovery_info is None: + return + add_devices(VolvoSensor(hass, discovery_info, sensor) + for sensor in SENSORS) + + +class VolvoSensor(VolvoEntity, BinarySensorDevice): + """Representation of a Volvo sensor.""" + + def __init__(self, hass, vehicle, sensor): + """Initialize the sensor.""" + super().__init__(hass, vehicle) + self._sensor = sensor + + @property + def _name(self): + """Name of sensor.""" + return self._sensor[1] + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + attr = self._sensor[0] + val = getattr(self.vehicle, attr) + if attr == 'bulb_failures': + return len(val) > 0 + elif attr in ['doors', 'windows']: + return any([val[key] for key in val if 'Open' in key]) + else: + return val != 'Normal' + + @property + def device_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return 'safety' diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index 834ec7e55bd..3212d74cf7c 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -1,97 +1,35 @@ """ -Support for Volvo On Call. +Support for tracking a Volvo. -http://www.volvocars.com/intl/own/owner-info/volvo-on-call For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.volvooncall/ """ import logging -from datetime import timedelta -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.util.dt import utcnow from homeassistant.util import slugify -from homeassistant.const import ( - CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME) -from homeassistant.components.device_tracker import ( - DEFAULT_SCAN_INTERVAL, PLATFORM_SCHEMA) - -MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1) +from homeassistant.components.volvooncall import DOMAIN _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['volvooncall==0.1.1'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, -}) - def setup_scanner(hass, config, see, discovery_info=None): - """Validate the configuration and return a scanner.""" - from volvooncall import Connection - connection = Connection( - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + """Setup Volvo tracker.""" + if discovery_info is None: + return - interval = max(MIN_TIME_BETWEEN_SCANS, - config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) + vin = discovery_info + vehicle = hass.data[DOMAIN].vehicles[vin] - def _see_vehicle(vehicle): - position = vehicle["position"] - dev_id = "volvo_" + slugify(vehicle["registrationNumber"]) - host_name = "%s (%s/%s)" % ( - vehicle["registrationNumber"], - vehicle["vehicleType"], - vehicle["modelYear"]) - - def any_opened(door): - """True if any door/window is opened.""" - return any([door[key] for key in door if "Open" in key]) - - attributes = dict( - unlocked=not vehicle["carLocked"], - tank_volume=vehicle["fuelTankVolume"], - average_fuel_consumption=round( - vehicle["averageFuelConsumption"] / 10, 1), # l/100km - washer_fluid_low=vehicle["washerFluidLevel"] != "Normal", - brake_fluid_low=vehicle["brakeFluid"] != "Normal", - service_warning=vehicle["serviceWarningStatus"] != "Normal", - bulb_failures=len(vehicle["bulbFailures"]) > 0, - doors_open=any_opened(vehicle["doors"]), - windows_open=any_opened(vehicle["windows"]), - fuel=vehicle["fuelAmount"], - odometer=round(vehicle["odometer"] / 1000), # km - range=vehicle["distanceToEmpty"]) - - if "heater" in vehicle and \ - "status" in vehicle["heater"]: - attributes.update(heater_on=vehicle["heater"]["status"] != "off") + host_name = vehicle.registration_number + dev_id = 'volvo_' + slugify(host_name) + def see_vehicle(vehicle): + """Callback for reporting vehicle position.""" see(dev_id=dev_id, host_name=host_name, - gps=(position["latitude"], - position["longitude"]), - attributes=attributes) + gps=(vehicle.position['latitude'], + vehicle.position['longitude'])) - def update(now): - """Update status from the online service.""" - _LOGGER.info("Updating") - try: - res, vehicles = connection.update() - if not res: - _LOGGER.error("Could not query server") - return False + hass.data[DOMAIN].entities[vin].append(see_vehicle) - for vehicle in vehicles: - _see_vehicle(vehicle) - - return True - finally: - track_point_in_utc_time(hass, update, now + interval) - - _LOGGER.info('Logging in to service') - return update(utcnow()) + return True diff --git a/homeassistant/components/lock/volvooncall.py b/homeassistant/components/lock/volvooncall.py new file mode 100644 index 00000000000..82632ad8e94 --- /dev/null +++ b/homeassistant/components/lock/volvooncall.py @@ -0,0 +1,42 @@ +""" +Support for Volvo locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.volvooncall/ +""" +import logging + +from homeassistant.components.lock import LockDevice +from homeassistant.components.volvooncall import VolvoEntity + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the lock.""" + if discovery_info is None: + return + add_devices([VolvoLock(hass, discovery_info)]) + + +class VolvoLock(VolvoEntity, LockDevice): + """Represents a car lock.""" + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self.vehicle.is_locked + + def lock(self, **kwargs): + """Lock the car.""" + self.vehicle.lock() + + def unlock(self, **kwargs): + """Unlock the car.""" + self.vehicle.unlock() + + @property + def _name(self): + """Return name.""" + return 'Lock' diff --git a/homeassistant/components/sensor/volvooncall.py b/homeassistant/components/sensor/volvooncall.py new file mode 100644 index 00000000000..82de6ac6876 --- /dev/null +++ b/homeassistant/components/sensor/volvooncall.py @@ -0,0 +1,59 @@ +""" +Support for VOC. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.volvooncall/ + +""" +import logging + +from homeassistant.components.volvooncall import VolvoEntity + +_LOGGER = logging.getLogger(__name__) + +SENSORS = [('odometer', 'Odometer', 'km', 'mdi:speedometer'), + ('fuel_amount', 'Fuel', 'L', 'mdi:gas-station'), + ('fuel_amount_level', 'Fuel', '%', 'mdi:water-percent'), + ('distance_to_empty', 'Range', 'km', 'mdi:ruler')] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Volvo sensors.""" + if discovery_info is None: + return + add_devices(VolvoSensor(hass, discovery_info, sensor) + for sensor in SENSORS) + + +class VolvoSensor(VolvoEntity): + """Representation of a Volvo sensor.""" + + def __init__(self, hass, vehicle, sensor): + """Initialize sensor.""" + super().__init__(hass, vehicle) + self._sensor = sensor + + @property + def state(self): + """Return the state of the sensor.""" + attr = self._sensor[0] + val = getattr(self.vehicle, attr) + if attr == 'odometer': + return round(val / 1000) # km + else: + return val + + @property + def _name(self): + """Name of quantity.""" + return self._sensor[1] + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._sensor[2] + + @property + def icon(self): + """Return the icon.""" + return self._sensor[3] diff --git a/homeassistant/components/switch/volvooncall.py b/homeassistant/components/switch/volvooncall.py new file mode 100644 index 00000000000..a13f8780741 --- /dev/null +++ b/homeassistant/components/switch/volvooncall.py @@ -0,0 +1,47 @@ +""" +Support for Volvo heater. + +This platform uses the Telldus Live online service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.volvooncall/ +""" +import logging + +from homeassistant.components.volvooncall import VolvoEntity +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Tellstick switches.""" + if discovery_info is None: + return + add_devices([VolvoSwitch(hass, discovery_info)]) + + +class VolvoSwitch(VolvoEntity, ToggleEntity): + """Representation of a Volvo switch.""" + + @property + def is_on(self): + """Return true if switch is on.""" + return self.vehicle.is_heater_on + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self.vehicle.start_heater() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self.vehicle.stop_heater() + + @property + def _name(self): + """Return the name of the switch.""" + return 'Heater' + + def icon(self): + """Return the icon.""" + return 'mdi:radiator' diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py new file mode 100644 index 00000000000..094c660c66d --- /dev/null +++ b/homeassistant/components/volvooncall.py @@ -0,0 +1,148 @@ +""" +Support for Volvo On Call. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/volvooncall/ +""" + +from datetime import timedelta +import logging + +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow +import voluptuous as vol + +DOMAIN = 'volvooncall' + +REQUIREMENTS = ['volvooncall==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_UPDATE_INTERVAL = 'update_interval' +MIN_UPDATE_INTERVAL = timedelta(minutes=1) +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))) + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the VOC component.""" + from volvooncall import Connection + connection = Connection( + config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD)) + + interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) + + class state: # pylint:disable=invalid-name + """Namespace to hold state for each vehicle.""" + + entities = {} + vehicles = {} + + hass.data[DOMAIN] = state + + def discover_vehicle(vehicle): + """Load relevant platforms.""" + state.entities[vehicle.vin] = [] + components = ['sensor', 'binary_sensor'] + + if getattr(vehicle, 'position'): + components.append('device_tracker') + + if vehicle.heater_supported: + components.append('switch') + + if vehicle.lock_supported: + components.append('lock') + + for component in components: + discovery.load_platform(hass, + component, + DOMAIN, + vehicle.vin, + config) + + def update_vehicle(vehicle): + """Updated information on vehicle received.""" + state.vehicles[vehicle.vin] = vehicle + if vehicle.vin not in state.entities: + discover_vehicle(vehicle) + + for entity in state.entities[vehicle.vin]: + if isinstance(entity, Entity): + entity.schedule_update_ha_state() + else: + entity(vehicle) # device tracker + + def update(now): + """Update status from the online service.""" + try: + if not connection.update(): + _LOGGER.warning('Could not query server') + return False + + for vehicle in connection.vehicles: + update_vehicle(vehicle) + + return True + finally: + track_point_in_utc_time(hass, update, now + interval) + + _LOGGER.info('Logging in to service') + return update(utcnow()) + + +class VolvoEntity(Entity): + """Base class for all VOC entities.""" + + def __init__(self, hass, vin): + """Initialize the entity.""" + self._hass = hass + self._vin = vin + self._hass.data[DOMAIN].entities[self._vin].append(self) + + @property + def vehicle(self): + """Return vehicle.""" + return self._hass.data[DOMAIN].vehicles[self._vin] + + @property + def name(self): + """Return the name of the sensor.""" + return '%s %s' % ( + self.vehicle.registration_number, + self._name) + + @property + def _name(self): + """Overridden by subclasses.""" + return None + + @property + def should_poll(self): + """Polling is not needed.""" + 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.""" + return dict(model='%s/%s' % ( + self.vehicle.vehicle_type, + self.vehicle.model_year)) diff --git a/requirements_all.txt b/requirements_all.txt index f394cb705cf..77ab63d5010 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -710,8 +710,8 @@ urllib3 # homeassistant.components.camera.uvc uvcclient==0.10.0 -# homeassistant.components.device_tracker.volvooncall -volvooncall==0.1.1 +# homeassistant.components.volvooncall +volvooncall==0.3.0 # homeassistant.components.verisure vsure==0.11.1