Merge pull request #771 from MartinHjelmare/mysensors-component-switch

Mysensors component switch
This commit is contained in:
Paulus Schoutsen 2016-01-11 19:04:35 -08:00
commit a5b198e2b8
7 changed files with 538 additions and 113 deletions

View File

@ -36,6 +36,9 @@ omit =
homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py
homeassistant/components/mysensors.py
homeassistant/components/*/mysensors.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
@ -92,7 +95,6 @@ omit =
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/rpi_gpio.py

View File

@ -0,0 +1,230 @@
"""
homeassistant.components.mysensors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MySensors component that connects to a MySensors gateway via pymysensors
API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mysensors.html
New features:
New MySensors component.
Updated MySensors Sensor platform.
New MySensors Switch platform. Currently only in optimistic mode (compare
with MQTT).
Multiple gateways are now supported.
Configuration.yaml:
mysensors:
gateways:
- port: '/dev/ttyUSB0'
persistence_file: 'path/mysensors.json'
- port: '/dev/ttyACM1'
persistence_file: 'path/mysensors2.json'
debug: true
persistence: true
version: '1.5'
"""
import logging
from homeassistant.helpers import validate_config
import homeassistant.bootstrap as bootstrap
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED,
TEMP_CELCIUS,)
CONF_GATEWAYS = 'gateways'
CONF_PORT = 'port'
CONF_DEBUG = 'debug'
CONF_PERSISTENCE = 'persistence'
CONF_PERSISTENCE_FILE = 'persistence_file'
CONF_VERSION = 'version'
DEFAULT_VERSION = '1.4'
DOMAIN = 'mysensors'
DEPENDENCIES = []
REQUIREMENTS = [
'https://github.com/theolind/pymysensors/archive/'
'005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4']
_LOGGER = logging.getLogger(__name__)
ATTR_NODE_ID = 'node_id'
ATTR_CHILD_ID = 'child_id'
ATTR_PORT = 'port'
GATEWAYS = None
SCAN_INTERVAL = 30
DISCOVER_SENSORS = "mysensors.sensors"
DISCOVER_SWITCHES = "mysensors.switches"
# Maps discovered services to their platforms
DISCOVERY_COMPONENTS = [
('sensor', DISCOVER_SENSORS),
('switch', DISCOVER_SWITCHES),
]
def setup(hass, config):
"""Setup the MySensors component."""
# pylint: disable=too-many-locals
if not validate_config(config,
{DOMAIN: [CONF_GATEWAYS]},
_LOGGER):
return False
import mysensors.mysensors as mysensors
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
def setup_gateway(port, persistence, persistence_file, version):
"""Return gateway after setup of the gateway."""
gateway = mysensors.SerialGateway(port, event_callback=None,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version)
gateway.metric = is_metric
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
gateway = GatewayWrapper(gateway, version)
# pylint: disable=attribute-defined-outside-init
gateway.event_callback = gateway.callback_factory()
def gw_start(event):
"""Callback to trigger start of gateway and any persistence."""
gateway.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
if persistence:
for node_id in gateway.sensors:
gateway.event_callback('persistence', node_id)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start)
return gateway
# Setup all ports from config
global GATEWAYS
GATEWAYS = {}
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
if isinstance(conf_gateways, dict):
conf_gateways = [conf_gateways]
persistence = config[DOMAIN].get(CONF_PERSISTENCE, True)
for index, gway in enumerate(conf_gateways):
port = gway[CONF_PORT]
persistence_file = gway.get(
CONF_PERSISTENCE_FILE,
hass.config.path('mysensors{}.pickle'.format(index + 1)))
GATEWAYS[port] = setup_gateway(
port, persistence, persistence_file, version)
for (component, discovery_service) in DISCOVERY_COMPONENTS:
# Ensure component is loaded
if not bootstrap.setup_component(hass, component, config):
return False
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: discovery_service,
ATTR_DISCOVERED: {}})
return True
def pf_callback_factory(
s_types, v_types, devices, add_devices, entity_class):
"""Return a new callback for the platform."""
def mysensors_callback(gateway, node_id):
"""Callback for mysensors platform."""
if gateway.sensors[node_id].sketch_name is None:
_LOGGER.info('No sketch_name: node %s', node_id)
return
# previously discovered, just update state with latest info
if node_id in devices:
for entity in devices[node_id]:
entity.update_ha_state(True)
return
# First time we see this node, detect sensors
for child in gateway.sensors[node_id].children.values():
name = '{} {}.{}'.format(
gateway.sensors[node_id].sketch_name, node_id, child.id)
for value_type in child.values.keys():
if child.type not in s_types or value_type not in v_types:
continue
devices[node_id].append(
entity_class(gateway, node_id, child.id, name, value_type))
if devices[node_id]:
_LOGGER.info('adding new devices: %s', devices[node_id])
add_devices(devices[node_id])
for entity in devices[node_id]:
entity.update_ha_state(True)
return mysensors_callback
class GatewayWrapper(object):
"""Gateway wrapper class, by subclassing serial gateway."""
def __init__(self, gateway, version):
"""Setup class attributes on instantiation.
Args:
gateway (mysensors.SerialGateway): Gateway to wrap.
version (str): Version of mysensors API.
Attributes:
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
version (str): Version of mysensors API.
platform_callbacks (list): Callback functions, one per platform.
const (module): Mysensors API constants.
__initialised (bool): True if GatewayWrapper is initialised.
"""
self._wrapped_gateway = gateway
self.version = version
self.platform_callbacks = []
self.const = self.get_const()
self.__initialised = True
def __getattr__(self, name):
"""See if this object has attribute name."""
# Do not use hasattr, it goes into infinite recurrsion
if name in self.__dict__:
# this object has it
return getattr(self, name)
# proxy to the wrapped object
return getattr(self._wrapped_gateway, name)
def __setattr__(self, name, value):
"""See if this object has attribute name then set to value."""
if '_GatewayWrapper__initialised' not in self.__dict__:
return object.__setattr__(self, name, value)
elif name in self.__dict__:
object.__setattr__(self, name, value)
else:
object.__setattr__(self._wrapped_gateway, name, value)
def get_const(self):
"""Get mysensors API constants."""
if self.version == '1.5':
import mysensors.const_15 as const
else:
import mysensors.const_14 as const
return const
def callback_factory(self):
"""Return a new callback function."""
def node_update(update_type, node_id):
"""Callback for node updates from the MySensors gateway."""
_LOGGER.info('update %s: node %s', update_type, node_id)
for callback in self.platform_callbacks:
callback(self, node_id)
return node_update

View File

@ -9,8 +9,8 @@ https://home-assistant.io/components/sensor/
import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import (wink, zwave, isy994,
verisure, ecobee, tellduslive)
from homeassistant.components import (
wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors)
DOMAIN = 'sensor'
SCAN_INTERVAL = 30
@ -25,6 +25,7 @@ DISCOVERY_PLATFORMS = {
verisure.DISCOVER_SENSORS: 'verisure',
ecobee.DISCOVER_SENSORS: 'ecobee',
tellduslive.DISCOVER_SENSORS: 'tellduslive',
mysensors.DISCOVER_SENSORS: 'mysensors',
}

View File

@ -7,150 +7,177 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mysensors/
"""
import logging
from collections import defaultdict
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP,
ATTR_BATTERY_LEVEL,
TEMP_CELCIUS, TEMP_FAHRENHEIT,
STATE_ON, STATE_OFF)
CONF_PORT = "port"
CONF_DEBUG = "debug"
CONF_PERSISTENCE = "persistence"
CONF_PERSISTENCE_FILE = "persistence_file"
CONF_VERSION = "version"
ATTR_NODE_ID = "node_id"
ATTR_CHILD_ID = "child_id"
import homeassistant.components.mysensors as mysensors
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/'
'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip'
'#pymysensors==0.3']
DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Setup the mysensors platform. """
import mysensors.mysensors as mysensors
import mysensors.const_14 as const
devices = {} # keep track of devices added to HA
# Just assume celcius means that the user wants metric for now.
# It may make more sense to make this a global config option in the future.
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
def sensor_update(update_type, nid):
""" Callback for sensor updates from the MySensors gateway. """
_LOGGER.info("sensor_update %s: node %s", update_type, nid)
sensor = gateway.sensors[nid]
if sensor.sketch_name is None:
"""Setup the mysensors platform for sensors."""
# Only act if loaded via mysensors by discovery event.
# Otherwise gateway is not setup.
if discovery_info is None:
return
if nid not in devices:
devices[nid] = {}
node = devices[nid]
new_devices = []
for child_id, child in sensor.children.items():
if child_id not in node:
node[child_id] = {}
for value_type, value in child.values.items():
if value_type not in node[child_id]:
name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id)
node[child_id][value_type] = \
MySensorsNodeValue(
nid, child_id, name, value_type, is_metric, const)
new_devices.append(node[child_id][value_type])
else:
node[child_id][value_type].update_sensor(
value, sensor.battery_level)
for gateway in mysensors.GATEWAYS.values():
# Define the S_TYPES and V_TYPES that the platform should handle as
# states.
s_types = [
gateway.const.Presentation.S_TEMP,
gateway.const.Presentation.S_HUM,
gateway.const.Presentation.S_BARO,
gateway.const.Presentation.S_WIND,
gateway.const.Presentation.S_RAIN,
gateway.const.Presentation.S_UV,
gateway.const.Presentation.S_WEIGHT,
gateway.const.Presentation.S_POWER,
gateway.const.Presentation.S_DISTANCE,
gateway.const.Presentation.S_LIGHT_LEVEL,
gateway.const.Presentation.S_IR,
gateway.const.Presentation.S_WATER,
gateway.const.Presentation.S_AIR_QUALITY,
gateway.const.Presentation.S_CUSTOM,
gateway.const.Presentation.S_DUST,
gateway.const.Presentation.S_SCENE_CONTROLLER,
]
not_v_types = [
gateway.const.SetReq.V_ARMED,
gateway.const.SetReq.V_LIGHT,
gateway.const.SetReq.V_LOCK_STATUS,
]
if float(gateway.version) >= 1.5:
s_types.extend([
gateway.const.Presentation.S_COLOR_SENSOR,
gateway.const.Presentation.S_MULTIMETER,
])
not_v_types.extend([gateway.const.SetReq.V_STATUS, ])
v_types = [member for member in gateway.const.SetReq
if member.value not in not_v_types]
if new_devices:
_LOGGER.info("adding new devices: %s", new_devices)
add_devices(new_devices)
port = config.get(CONF_PORT)
if port is None:
_LOGGER.error("Missing required key 'port'")
return False
persistence = config.get(CONF_PERSISTENCE, True)
persistence_file = config.get(CONF_PERSISTENCE_FILE,
hass.config.path('mysensors.pickle'))
version = config.get(CONF_VERSION, '1.4')
gateway = mysensors.SerialGateway(port, sensor_update,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version)
gateway.metric = is_metric
gateway.debug = config.get(CONF_DEBUG, False)
gateway.start()
if persistence:
for nid in gateway.sensors:
sensor_update('sensor_update', nid)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
devices = defaultdict(list)
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
s_types, v_types, devices, add_devices, MySensorsSensor))
class MySensorsNodeValue(Entity):
""" Represents the value of a MySensors child node. """
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, node_id, child_id, name, value_type, metric, const):
self._name = name
class MySensorsSensor(Entity):
"""Represent the value of a MySensors child node."""
# pylint: disable=too-many-arguments
def __init__(self, gateway, node_id, child_id, name, value_type):
"""Setup class attributes on instantiation.
Args:
gateway (GatewayWrapper): Gateway object.
node_id (str): Id of node.
child_id (str): Id of child.
name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
Attributes:
gateway (GatewayWrapper): Gateway object.
node_id (str): Id of node.
child_id (str): Id of child.
_name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
battery_level (int): Node battery level.
_values (dict): Child values. Non state values set as state attributes.
"""
self.gateway = gateway
self.node_id = node_id
self.child_id = child_id
self.battery_level = 0
self._name = name
self.value_type = value_type
self.metric = metric
self._value = ''
self.const = const
self.battery_level = 0
self._values = {}
@property
def should_poll(self):
""" MySensor gateway pushes its state to HA. """
"""MySensor gateway pushes its state to HA."""
return False
@property
def name(self):
""" The name of this sensor. """
"""The name of this entity."""
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._value
"""Return the state of the device."""
if not self._values:
return ''
return self._values[self.value_type]
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity. """
if self.value_type == self.const.SetReq.V_TEMP:
return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT
elif self.value_type == self.const.SetReq.V_HUM or \
self.value_type == self.const.SetReq.V_DIMMER or \
self.value_type == self.const.SetReq.V_LIGHT_LEVEL:
"""Unit of measurement of this entity."""
# pylint:disable=too-many-return-statements
if self.value_type == self.gateway.const.SetReq.V_TEMP:
return TEMP_CELCIUS if self.gateway.metric else TEMP_FAHRENHEIT
elif self.value_type == self.gateway.const.SetReq.V_HUM or \
self.value_type == self.gateway.const.SetReq.V_DIMMER or \
self.value_type == self.gateway.const.SetReq.V_PERCENTAGE or \
self.value_type == self.gateway.const.SetReq.V_LIGHT_LEVEL:
return '%'
elif self.value_type == self.gateway.const.SetReq.V_WATT:
return 'W'
elif self.value_type == self.gateway.const.SetReq.V_KWH:
return 'kWh'
elif self.value_type == self.gateway.const.SetReq.V_VOLTAGE:
return 'V'
elif self.value_type == self.gateway.const.SetReq.V_CURRENT:
return 'A'
elif self.value_type == self.gateway.const.SetReq.V_IMPEDANCE:
return 'ohm'
elif self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
return self._values[self.gateway.const.SetReq.V_UNIT_PREFIX]
return None
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
device_attr = dict(self._values)
device_attr.pop(self.value_type, None)
return device_attr
@property
def state_attributes(self):
""" Returns the state attributes. """
return {
ATTR_NODE_ID: self.node_id,
ATTR_CHILD_ID: self.child_id,
"""Return the state attributes."""
data = {
mysensors.ATTR_PORT: self.gateway.port,
mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
def update_sensor(self, value, battery_level):
""" Update a sensor with the latest value from the controller. """
_LOGGER.info("%s value = %s", self._name, value)
if self.value_type == self.const.SetReq.V_TRIPPED or \
self.value_type == self.const.SetReq.V_ARMED:
self._value = STATE_ON if int(value) == 1 else STATE_OFF
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data
def update(self):
"""Update the controller with the latest values from a sensor."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
for value_type, value in child.values.items():
_LOGGER.info(
"%s: value_type %s, value = %s", self._name, value_type, value)
if value_type == self.gateway.const.SetReq.V_TRIPPED:
self._values[value_type] = STATE_ON if int(
value) == 1 else STATE_OFF
else:
self._value = value
self.battery_level = battery_level
self.update_ha_state()
self._values[value_type] = value
self.battery_level = node.battery_level

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import (
group, discovery, wink, isy994, verisure, zwave, tellduslive)
group, discovery, wink, isy994, verisure, zwave, tellduslive, mysensors)
DOMAIN = 'switch'
SCAN_INTERVAL = 30
@ -41,6 +41,7 @@ DISCOVERY_PLATFORMS = {
verisure.DISCOVER_SWITCHES: 'verisure',
zwave.DISCOVER_SWITCHES: 'zwave',
tellduslive.DISCOVER_SWITCHES: 'tellduslive',
mysensors.DISCOVER_SWITCHES: 'mysensors',
}
PROP_TO_ATTR = {

View File

@ -0,0 +1,164 @@
"""
homeassistant.components.switch.mysensors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for MySensors switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mysensors.html
"""
import logging
from collections import defaultdict
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
STATE_ON, STATE_OFF)
import homeassistant.components.mysensors as mysensors
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors platform for switches."""
# Only act if loaded via mysensors by discovery event.
# Otherwise gateway is not setup.
if discovery_info is None:
return
for gateway in mysensors.GATEWAYS.values():
# Define the S_TYPES and V_TYPES that the platform should handle as
# states.
s_types = [
gateway.const.Presentation.S_DOOR,
gateway.const.Presentation.S_MOTION,
gateway.const.Presentation.S_SMOKE,
gateway.const.Presentation.S_LIGHT,
gateway.const.Presentation.S_LOCK,
]
v_types = [
gateway.const.SetReq.V_ARMED,
gateway.const.SetReq.V_LIGHT,
gateway.const.SetReq.V_LOCK_STATUS,
]
if float(gateway.version) >= 1.5:
s_types.extend([
gateway.const.Presentation.S_BINARY,
gateway.const.Presentation.S_SPRINKLER,
gateway.const.Presentation.S_WATER_LEAK,
gateway.const.Presentation.S_SOUND,
gateway.const.Presentation.S_VIBRATION,
gateway.const.Presentation.S_MOISTURE,
])
v_types.extend([gateway.const.SetReq.V_STATUS, ])
devices = defaultdict(list)
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
s_types, v_types, devices, add_devices, MySensorsSwitch))
class MySensorsSwitch(SwitchDevice):
"""Represent the value of a MySensors child node."""
# pylint: disable=too-many-arguments
def __init__(self, gateway, node_id, child_id, name, value_type):
"""Setup class attributes on instantiation.
Args:
gateway (GatewayWrapper): Gateway object.
node_id (str): Id of node.
child_id (str): Id of child.
name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
Attributes:
gateway (GatewayWrapper): Gateway object
node_id (str): Id of node.
child_id (str): Id of child.
_name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
battery_level (int): Node battery level.
_values (dict): Child values. Non state values set as state attributes.
"""
self.gateway = gateway
self.node_id = node_id
self.child_id = child_id
self._name = name
self.value_type = value_type
self.battery_level = 0
self._values = {}
@property
def should_poll(self):
"""MySensor gateway pushes its state to HA."""
return False
@property
def name(self):
"""The name of this entity."""
return self._name
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
device_attr = dict(self._values)
device_attr.pop(self.value_type, None)
return device_attr
@property
def state_attributes(self):
"""Return the state attributes."""
data = {
mysensors.ATTR_PORT: self.gateway.port,
mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data
@property
def is_on(self):
"""Return True if switch is on."""
if self.value_type in self._values:
return self._values[self.value_type] == STATE_ON
return False
def turn_on(self):
"""Turn the switch on."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 1)
self._values[self.value_type] = STATE_ON
self.update_ha_state()
def turn_off(self):
"""Turn the switch off."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 0)
self._values[self.value_type] = STATE_OFF
self.update_ha_state()
def update(self):
"""Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
for value_type, value in child.values.items():
_LOGGER.info(
"%s: value_type %s, value = %s", self._name, value_type, value)
if value_type == self.gateway.const.SetReq.V_ARMED or \
value_type == self.gateway.const.SetReq.V_STATUS or \
value_type == self.gateway.const.SetReq.V_LIGHT or \
value_type == self.gateway.const.SetReq.V_LOCK_STATUS:
self._values[value_type] = (
STATE_ON if int(value) == 1 else STATE_OFF)
else:
self._values[value_type] = value
self.battery_level = node.battery_level

View File

@ -89,6 +89,9 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6
# homeassistant.components.mqtt
paho-mqtt==1.1
# homeassistant.components.mysensors
https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4
# homeassistant.components.notify.pushbullet
pushbullet.py==0.9.0
@ -131,9 +134,6 @@ eliqonline==1.0.11
# homeassistant.components.sensor.forecast
python-forecastio==1.3.3
# homeassistant.components.sensor.mysensors
https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3
# homeassistant.components.sensor.openweathermap
pyowm==2.3.0