From 5f445b4a13ca5284d525acc977589d299eb0ce9c Mon Sep 17 00:00:00 2001 From: Sergey Isachenko Date: Thu, 31 Aug 2017 07:13:02 +0300 Subject: [PATCH] Tesla platform (#9211) * Tesla support implemetation * requirements_all.txt fix * .coveragerc fix * logging-too-many-args fix * logging-too-many-args attempt 2 * Post-review fixes. * requirements version fix * requirements * Lint fix * Hot fix * requirements_all.txt fix * Review preparation. * 1. Linting fix. 2. Minimal value for SCAN_INTERVAL hardcoded to 300 sec (to prevent possible ban form Tesla) * Removed redundant whitespace. * Fixed components according to @MartinHjelmare proposals and remarks. * .coveragerc as @MartinHjelmare suggested. * Minor changes * Fix docstrings * Update ordering * Update quotes * Minor changes * Update quotes --- .coveragerc | 3 + .../components/binary_sensor/tesla.py | 57 +++++++++++ homeassistant/components/climate/tesla.py | 93 ++++++++++++++++++ .../components/device_tracker/tesla.py | 57 +++++++++++ homeassistant/components/lock/tesla.py | 57 +++++++++++ homeassistant/components/sensor/tesla.py | 82 ++++++++++++++++ homeassistant/components/tesla.py | 95 +++++++++++++++++++ requirements_all.txt | 3 + 8 files changed, 447 insertions(+) create mode 100644 homeassistant/components/binary_sensor/tesla.py create mode 100644 homeassistant/components/climate/tesla.py create mode 100644 homeassistant/components/device_tracker/tesla.py create mode 100644 homeassistant/components/lock/tesla.py create mode 100644 homeassistant/components/sensor/tesla.py create mode 100644 homeassistant/components/tesla.py diff --git a/.coveragerc b/.coveragerc index 37d4fd831dc..b43688aa281 100644 --- a/.coveragerc +++ b/.coveragerc @@ -170,6 +170,9 @@ omit = homeassistant/components/tellstick.py homeassistant/components/*/tellstick.py + homeassistant/components/tesla.py + homeassistant/components/*/tesla.py + homeassistant/components/*/thinkingcleaner.py homeassistant/components/tradfri.py diff --git a/homeassistant/components/binary_sensor/tesla.py b/homeassistant/components/binary_sensor/tesla.py new file mode 100644 index 00000000000..af7e394b50e --- /dev/null +++ b/homeassistant/components/binary_sensor/tesla.py @@ -0,0 +1,57 @@ +""" +Support for Tesla binary sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.tesla/ +""" +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, ENTITY_ID_FORMAT) +from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tesla'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tesla binary sensor.""" + devices = [ + TeslaBinarySensor( + device, hass.data[TESLA_DOMAIN]['controller'], 'connectivity') + for device in hass.data[TESLA_DOMAIN]['devices']['binary_sensor']] + add_devices(devices, True) + + +class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): + """Implement an Tesla binary sensor for parking and charger.""" + + def __init__(self, tesla_device, controller, sensor_type): + """Initialisation of binary sensor.""" + super().__init__(tesla_device, controller) + self._name = self.tesla_device.name + self._state = False + self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) + self._sensor_type = sensor_type + + @property + def device_class(self): + """Return the class of this binary sensor.""" + return self._sensor_type + + @property + def name(self): + """Return the name of the binary sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state + + def update(self): + """Update the state of the device.""" + _LOGGER.debug("Updating sensor: %s", self._name) + self.tesla_device.update() + self._state = self.tesla_device.get_value() diff --git a/homeassistant/components/climate/tesla.py b/homeassistant/components/climate/tesla.py new file mode 100644 index 00000000000..39d002e72d9 --- /dev/null +++ b/homeassistant/components/climate/tesla.py @@ -0,0 +1,93 @@ +""" +Support for Tesla HVAC system. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/climate.tesla/ +""" +import logging + +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice +from homeassistant.const import ( + TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tesla'] + +OPERATION_LIST = [STATE_ON, STATE_OFF] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tesla climate platform.""" + devices = [TeslaThermostat(device, hass.data[TESLA_DOMAIN]['controller']) + for device in hass.data[TESLA_DOMAIN]['devices']['climate']] + add_devices(devices, True) + + +class TeslaThermostat(TeslaDevice, ClimateDevice): + """Representation of a Tesla climate.""" + + def __init__(self, tesla_device, controller): + """Initialize the Tesla device.""" + super().__init__(tesla_device, controller) + self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) + self._target_temperature = None + self._temperature = None + self._name = self.tesla_device.name + + @property + def current_operation(self): + """Return current operation ie. On or Off.""" + mode = self.tesla_device.is_hvac_enabled() + if mode: + return OPERATION_LIST[0] # On + else: + return OPERATION_LIST[1] # Off + + @property + def operation_list(self): + """List of available operation modes.""" + return OPERATION_LIST + + def update(self): + """Called by the Tesla device callback to update state.""" + _LOGGER.debug("Updating: %s", self._name) + self.tesla_device.update() + self._target_temperature = self.tesla_device.get_goal_temp() + self._temperature = self.tesla_device.get_current_temp() + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + tesla_temp_units = self.tesla_device.measurement + + if tesla_temp_units == 'F': + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + _LOGGER.debug("Setting temperature for: %s", self._name) + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature: + self.tesla_device.set_temperature(temperature) + + def set_operation_mode(self, operation_mode): + """Set HVAC mode (auto, cool, heat, off).""" + _LOGGER.debug("Setting mode for: %s", self._name) + if operation_mode == OPERATION_LIST[1]: # off + self.tesla_device.set_status(False) + elif operation_mode == OPERATION_LIST[0]: # heat + self.tesla_device.set_status(True) diff --git a/homeassistant/components/device_tracker/tesla.py b/homeassistant/components/device_tracker/tesla.py new file mode 100644 index 00000000000..4945e98a94d --- /dev/null +++ b/homeassistant/components/device_tracker/tesla.py @@ -0,0 +1,57 @@ +""" +Support for the Tesla platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.tesla/ +""" +import logging + +from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN +from homeassistant.helpers.event import track_utc_time_change +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tesla'] + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the Tesla tracker.""" + TeslaDeviceTracker( + hass, config, see, + hass.data[TESLA_DOMAIN]['devices']['devices_tracker']) + return True + + +class TeslaDeviceTracker(object): + """A class representing a Tesla device.""" + + def __init__(self, hass, config, see, tesla_devices): + """Initialize the Tesla device scanner.""" + self.hass = hass + self.see = see + self.devices = tesla_devices + self._update_info() + + track_utc_time_change( + self.hass, self._update_info, second=range(0, 60, 30)) + + def _update_info(self, now=None): + """Update the device info.""" + for device in self.devices: + device.update() + name = device.name + _LOGGER.debug("Updating device position: %s", name) + dev_id = slugify(device.uniq_name) + location = device.get_location() + lat = location['latitude'] + lon = location['longitude'] + attrs = { + 'trackr_id': dev_id, + 'id': dev_id, + 'name': name + } + self.see( + dev_id=dev_id, host_name=name, + gps=(lat, lon), attributes=attrs + ) diff --git a/homeassistant/components/lock/tesla.py b/homeassistant/components/lock/tesla.py new file mode 100644 index 00000000000..3e93e4787a0 --- /dev/null +++ b/homeassistant/components/lock/tesla.py @@ -0,0 +1,57 @@ +""" +Support for Tesla door locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.tesla/ +""" +import logging + +from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice +from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tesla'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tesla lock platform.""" + devices = [TeslaLock(device, hass.data[TESLA_DOMAIN]['controller']) + for device in hass.data[TESLA_DOMAIN]['devices']['lock']] + add_devices(devices, True) + + +class TeslaLock(TeslaDevice, LockDevice): + """Representation of a Tesla door lock.""" + + def __init__(self, tesla_device, controller): + """Initialisation of the lock.""" + self._state = None + super().__init__(tesla_device, controller) + self._name = self.tesla_device.name + self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) + + def lock(self, **kwargs): + """Send the lock command.""" + _LOGGER.debug("Locking doors for: %s", self._name) + self.tesla_device.lock() + self._state = STATE_LOCKED + + def unlock(self, **kwargs): + """Send the unlock command.""" + _LOGGER.debug("Unlocking doors for: %s", self._name) + self.tesla_device.unlock() + self._state = STATE_UNLOCKED + + @property + def is_locked(self): + """Get whether the lock is in locked state.""" + return self._state == STATE_LOCKED + + def update(self): + """Updating state of the lock.""" + _LOGGER.debug("Updating state for: %s", self._name) + self.tesla_device.update() + self._state = STATE_LOCKED if self.tesla_device.is_locked() \ + else STATE_UNLOCKED diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py new file mode 100644 index 00000000000..fc31a5543e2 --- /dev/null +++ b/homeassistant/components/sensor/tesla.py @@ -0,0 +1,82 @@ +""" +Sensors for the Tesla sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tesla/ +""" +import logging +from datetime import timedelta + +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tesla'] + +SCAN_INTERVAL = timedelta(minutes=5) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tesla sensor platform.""" + controller = hass.data[TESLA_DOMAIN]['devices']['controller'] + devices = [] + + for device in hass.data[TESLA_DOMAIN]['devices']['sensor']: + if device.bin_type == 0x4: + devices.append(TeslaSensor(device, controller, 'inside')) + devices.append(TeslaSensor(device, controller, 'outside')) + else: + devices.append(TeslaSensor(device, controller)) + add_devices(devices, True) + + +class TeslaSensor(TeslaDevice, Entity): + """Representation of Tesla sensors.""" + + def __init__(self, tesla_device, controller, sensor_type=None): + """Initialisation of the sensor.""" + self.current_value = None + self._temperature_units = None + self.last_changed_time = None + self.type = sensor_type + super().__init__(tesla_device, controller) + + if self.type: + self._name = '{} ({})'.format(self.tesla_device.name, self.type) + self.entity_id = ENTITY_ID_FORMAT.format( + '{}_{}'.format(self.tesla_id, self.type)) + else: + self._name = self.tesla_device.name + self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) + + @property + def state(self): + """Return the state of the sensor.""" + return self.current_value + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of the device.""" + return self._temperature_units + + def update(self): + """Update the state from the sensor.""" + _LOGGER.debug("Updating sensor: %s", self._name) + self.tesla_device.update() + if self.tesla_device.bin_type == 0x4: + if self.type == 'outside': + self.current_value = self.tesla_device.get_outside_temp() + else: + self.current_value = self.tesla_device.get_inside_temp() + + tesla_temp_units = self.tesla_device.measurement + + if tesla_temp_units == 'F': + self._temperature_units = TEMP_FAHRENHEIT + else: + self._temperature_units = TEMP_CELSIUS + else: + self.current_value = self.tesla_device.battery_level() diff --git a/homeassistant/components/tesla.py b/homeassistant/components/tesla.py new file mode 100644 index 00000000000..e48d805abab --- /dev/null +++ b/homeassistant/components/tesla.py @@ -0,0 +1,95 @@ +""" +Support for Tesla cars. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tesla/ +""" +from collections import defaultdict + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) +from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +REQUIREMENTS = ['teslajsonpy==0.0.11'] + +DOMAIN = 'tesla' + +TESLA_ID_FORMAT = '{}_{}' +TESLA_ID_LIST_SCHEMA = vol.Schema([int]) + +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=300): + vol.All(cv.positive_int, vol.Clamp(min=300)), + }), +}, extra=vol.ALLOW_EXTRA) + +TESLA_COMPONENTS = [ + 'sensor', 'lock', 'climate', 'binary_sensor', 'device_tracker' +] + + +def setup(hass, base_config): + """Set up of Tesla platform.""" + from teslajsonpy.controller import Controller as teslaApi + + config = base_config.get(DOMAIN) + + email = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + update_interval = config.get(CONF_SCAN_INTERVAL) + if hass.data.get(DOMAIN) is None: + hass.data[DOMAIN] = { + 'controller': teslaApi(email, password, update_interval), + 'devices': defaultdict(list) + } + + all_devices = hass.data[DOMAIN]['controller'].list_vehicles() + + if not all_devices: + return False + + for device in all_devices: + hass.data[DOMAIN]['devices'][device.hass_type].append(device) + + for component in TESLA_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, base_config) + + return True + + +class TeslaDevice(Entity): + """Representation of a Tesla device.""" + + def __init__(self, tesla_device, controller): + """Initialisation of the Tesla device.""" + self.tesla_device = tesla_device + self.controller = controller + self._name = self.tesla_device.name + self.tesla_id = slugify(self.tesla_device.uniq_name) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Get polling requirement from tesla device.""" + return self.tesla_device.should_poll + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + + if self.tesla_device.has_battery(): + attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() + return attr diff --git a/requirements_all.txt b/requirements_all.txt index a2bc1260aaa..01b38661af7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -946,6 +946,9 @@ tellduslive==0.3.4 # homeassistant.components.sensor.temper temperusb==1.5.3 +# homeassistant.components.tesla +teslajsonpy==0.0.11 + # homeassistant.components.thingspeak thingspeak==0.4.1