diff --git a/.coveragerc b/.coveragerc index 3529e7413ca..7287dcb143f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -145,6 +145,9 @@ omit = homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py + homeassistant/components/mercedesme.py + homeassistant/components/*/mercedesme.py + homeassistant/components/mochad.py homeassistant/components/*/mochad.py diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py new file mode 100755 index 00000000000..dbbe679e852 --- /dev/null +++ b/homeassistant/components/binary_sensor/mercedesme.py @@ -0,0 +1,100 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import logging +import datetime + +from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.mercedesme import ( + DATA_MME, MercedesMeEntity, BINARY_SENSORS) + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + data = hass.data[DATA_MME].data + + if not data.cars: + _LOGGER.error("setup_platform data.cars is none") + return + + devices = [] + for car in data.cars: + for dev in BINARY_SENSORS: + devices.append(MercedesMEBinarySensor( + data, dev, dev, car["vin"], None)) + + add_devices(devices, True) + + +class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): + """Representation of a Sensor.""" + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state == "On" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._name == "windowsClosed": + return { + "windowStatusFrontLeft": self._car["windowStatusFrontLeft"], + "windowStatusFrontRight": self._car["windowStatusFrontRight"], + "windowStatusRearLeft": self._car["windowStatusRearLeft"], + "windowStatusRearRight": self._car["windowStatusRearRight"], + "originalValue": self._car[self._name], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + elif self._name == "tireWarningLight": + return { + "frontRightTirePressureKpa": + self._car["frontRightTirePressureKpa"], + "frontLeftTirePressureKpa": + self._car["frontLeftTirePressureKpa"], + "rearRightTirePressureKpa": + self._car["rearRightTirePressureKpa"], + "rearLeftTirePressureKpa": + self._car["rearLeftTirePressureKpa"], + "originalValue": self._car[self._name], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"], + } + return { + "originalValue": self._car[self._name], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + def update(self): + """Fetch new state data for the sensor.""" + _LOGGER.debug("Updating %s", self._name) + + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + result = False + + if self._name == "windowsClosed": + result = bool(self._car[self._name] == "CLOSED") + elif self._name == "tireWarningLight": + result = bool(self._car[self._name] != "INACTIVE") + else: + result = self._car[self._name] is True + + self._state = "On" if result else "Off" + + _LOGGER.debug("Updated %s Value: %s IsOn: %s", + self._name, self._state, self.is_on) diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py new file mode 100755 index 00000000000..c33cc239412 --- /dev/null +++ b/homeassistant/components/device_tracker/mercedesme.py @@ -0,0 +1,72 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import logging +from datetime import timedelta + +from homeassistant.components.mercedesme import DATA_MME +from homeassistant.helpers.event import track_time_interval +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['mercedesme'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the Mercedes ME tracker.""" + if discovery_info is None: + return False + + data = hass.data[DATA_MME].data + + if not data.cars: + return False + + MercedesMEDeviceTracker(hass, config, see, data) + + return True + + +class MercedesMEDeviceTracker(object): + """A class representing a Mercedes ME device tracker.""" + + def __init__(self, hass, config, see, data): + """Initialize the Mercedes ME device tracker.""" + self.hass = hass + self.see = see + self.data = data + self.update_info() + + track_time_interval( + self.hass, self.update_info, MIN_TIME_BETWEEN_SCANS) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def update_info(self, now=None): + """Update the device info.""" + for device in self.data.cars: + _LOGGER.debug("Updating %s", device["vin"]) + location = self.data.get_location(device["vin"]) + if location is None: + return False + dev_id = device["vin"] + name = device["license"] + + lat = location['positionLat']['value'] + lon = location['positionLong']['value'] + attrs = { + 'trackr_id': dev_id, + 'id': dev_id, + 'name': name + } + self.see( + dev_id=dev_id, host_name=name, + gps=(lat, lon), attributes=attrs + ) + + return True diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py new file mode 100755 index 00000000000..0a5825bb16d --- /dev/null +++ b/homeassistant/components/mercedesme.py @@ -0,0 +1,144 @@ +""" +Support for MercedesME System. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] + +_LOGGER = logging.getLogger(__name__) + +BINARY_SENSORS = [ + 'doorsClosed', + 'windowsClosed', + 'locked', + 'tireWarningLight' +] + +DATA_MME = 'mercedesme' +DOMAIN = 'mercedesme' + +NOTIFICATION_ID = 'mercedesme_integration_notification' +NOTIFICATION_TITLE = 'Mercedes me integration setup' + +SIGNAL_UPDATE_MERCEDESME = "mercedesme_update" + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=30): + vol.All(cv.positive_int, vol.Clamp(min=10)) + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up MercedesMe System.""" + from mercedesmejsonpy.controller import Controller + from mercedesmejsonpy import Exceptions + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + mercedesme_api = Controller(username, password, scan_interval) + if not mercedesme_api.is_valid_session: + raise Exceptions.MercedesMeException(500) + hass.data[DATA_MME] = MercedesMeHub(mercedesme_api) + except Exceptions.MercedesMeException as ex: + if ex.code == 401: + hass.components.persistent_notification.create( + "Error:
Please check username and password." + "You will need to restart Home Assistant after fixing.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + else: + hass.components.persistent_notification.create( + "Error:
Can't communicate with Mercedes me API.
" + "Error code: {} Reason: {}" + "You will need to restart Home Assistant after fixing." + "".format(ex.code, ex.message), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + + _LOGGER.error("Unable to communicate with Mercedes me API: %s", + ex.message) + return False + + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + def hub_refresh(event_time): + """Call Mercedes me API to refresh information.""" + _LOGGER.info("Updating Mercedes me component.") + hass.data[DATA_MME].data.update() + dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME) + + track_time_interval( + hass, + hub_refresh, + timedelta(seconds=scan_interval)) + + return True + + +class MercedesMeHub(object): + """Representation of a base MercedesMe device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class MercedesMeEntity(Entity): + """Entity class for MercedesMe devices.""" + + def __init__(self, data, internal_name, sensor_name, vin, unit): + """Initialize the MercedesMe entity.""" + self._car = None + self._data = data + self._state = False + self._name = sensor_name + self._internal_name = internal_name + self._unit = unit + self._vin = vin + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback) + + def _update_callback(self): + """Callback update method.""" + self.schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py new file mode 100755 index 00000000000..08183a01ba8 --- /dev/null +++ b/homeassistant/components/sensor/mercedesme.py @@ -0,0 +1,92 @@ +""" +Support for Mercedes cars with Mercedes ME. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mercedesme/ +""" +import logging +import datetime + +from homeassistant.const import LENGTH_KILOMETERS +from homeassistant.components.mercedesme import DATA_MME, MercedesMeEntity + + +DEPENDENCIES = ['mercedesme'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'fuelLevelPercent': ['Fuel Level', '%'], + 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], + 'latestTrip': ['Latest Trip', None], + 'odometerKm': ['Odometer', LENGTH_KILOMETERS], + 'serviceIntervalDays': ['Next Service', 'days'], + 'doorsClosed': ['doorsClosed', None], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + if discovery_info is None: + return + + data = hass.data[DATA_MME].data + + if not data.cars: + return + + devices = [] + for car in data.cars: + for key, value in sorted(SENSOR_TYPES.items()): + devices.append( + MercedesMESensor(data, key, value[0], car["vin"], value[1])) + + add_devices(devices, True) + + +class MercedesMESensor(MercedesMeEntity): + """Representation of a Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating %s", self._internal_name) + + self._car = next( + car for car in self._data.cars if car["vin"] == self._vin) + + if self._internal_name == "latestTrip": + self._state = self._car["latestTrip"]["id"] + else: + self._state = self._car[self._internal_name] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._internal_name == "latestTrip": + return { + "durationSeconds": + self._car["latestTrip"]["durationSeconds"], + "distanceTraveledKm": + self._car["latestTrip"]["distanceTraveledKm"], + "startedAt": datetime.datetime.fromtimestamp( + self._car["latestTrip"]["startedAt"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "averageSpeedKmPerHr": + self._car["latestTrip"]["averageSpeedKmPerHr"], + "finished": self._car["latestTrip"]["finished"], + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"] + ).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } + + return { + "lastUpdate": datetime.datetime.fromtimestamp( + self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), + "car": self._car["license"] + } diff --git a/requirements_all.txt b/requirements_all.txt index 30aeda2b35d..20dc82cb95a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,6 +477,9 @@ matrix-client==0.0.6 # homeassistant.components.maxcube maxcube-api==0.1.0 +# homeassistant.components.mercedesme +mercedesmejsonpy==0.1.2 + # homeassistant.components.notify.message_bird messagebird==1.2.0