Skip to content
Merged
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ omit =
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
homeassistant/components/sensor/usps.py
homeassistant/components/sensor/vasttrafik.py
Expand Down
75 changes: 75 additions & 0 deletions homeassistant/components/sensor/upnp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
Support for UPnP Sensors (IGD).

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.upnp/
"""
import logging

from homeassistant.components.upnp import DATA_UPNP, UNITS
from homeassistant.helpers.entity import Entity

_LOGGER = logging.getLogger(__name__)

# sensor_type: [friendly_name, convert_unit, icon]
SENSOR_TYPES = {
'byte_received': ['received bytes', True, 'mdi:server-network'],
'byte_sent': ['sent bytes', True, 'mdi:server-network'],
'packets_in': ['packets received', False, 'mdi:server-network'],
'packets_out': ['packets sent', False, 'mdi:server-network'],
}


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the IGD sensors."""
upnp = hass.data[DATA_UPNP]
unit = discovery_info['unit']
add_devices([
IGDSensor(upnp, t, unit if SENSOR_TYPES[t][1] else None)
for t in SENSOR_TYPES], True)


class IGDSensor(Entity):
"""Representation of a UPnP IGD sensor."""

def __init__(self, upnp, sensor_type, unit=""):
"""Initialize the IGD sensor."""
self._upnp = upnp
self.type = sensor_type
self.unit = unit
self.unit_factor = UNITS[unit] if unit is not None else 1
self._name = 'IGD {}'.format(SENSOR_TYPES[sensor_type][0])
self._state = None

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def state(self):
"""Return the state of the device."""
if self._state is None:
return None
return format(self._state / self.unit_factor, '.1f')

@property
def icon(self):
"""Icon to use in the frontend, if any."""
return SENSOR_TYPES[self.type][2]

@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self.unit

def update(self):
"""Get the latest information from the IGD."""
if self.type == "byte_received":
self._state = self._upnp.totalbytereceived()
elif self.type == "byte_sent":
self._state = self._upnp.totalbytesent()
elif self.type == "packets_in":
self._state = self._upnp.totalpacketreceived()
elif self.type == "packets_out":
self._state = self._upnp.totalpacketsent()
31 changes: 29 additions & 2 deletions homeassistant/components/upnp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
This module will attempt to open a port in your router for Home Assistant.
Will open a port in your router for Home Assistant and provide statistics.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/upnp/
Expand All @@ -10,14 +10,33 @@
import voluptuous as vol

from homeassistant.const import (EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery

REQUIREMENTS = ['miniupnpc==1.9']
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this install miniupnp ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes


_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['api']
DOMAIN = 'upnp'

DATA_UPNP = 'UPNP'

CONF_ENABLE_PORT_MAPPING = 'port_mapping'
CONF_UNITS = 'unit'

UNITS = {
"Bytes": 1,
"KBytes": 1024,
"MBytes": 1024**2,
"GBytes": 1024**3,
}

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({}),
DOMAIN: vol.Schema({
vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean,
vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS),
}),
}, extra=vol.ALLOW_EXTRA)


Expand All @@ -27,6 +46,7 @@ def setup(hass, config):
import miniupnpc

upnp = miniupnpc.UPnP()
hass.data[DATA_UPNP] = upnp

upnp.discoverdelay = 200
upnp.discover()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You cannot do I/O inside a coroutine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood hass.data would be a global dictionary and wouldn't involve any I/O

But I'm moving everything back to setup() and update()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning to data doesn't do I/O. This comment was about upnp.discover() call. Since you changed it back to no longer be a coroutine this comment can be ignored.

Expand All @@ -36,6 +56,13 @@ def setup(hass, config):
_LOGGER.exception("Error when attempting to discover an UPnP IGD")
return False

unit = config[DOMAIN].get(CONF_UNITS)
discovery.load_platform(hass, 'sensor', DOMAIN, {'unit': unit}, config)

port_mapping = config[DOMAIN].get(CONF_ENABLE_PORT_MAPPING)
if not port_mapping:
return True

base_url = urlsplit(hass.config.api.base_url)
host = base_url.hostname
external_port = internal_port = base_url.port
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,9 @@ mficlient==0.3.0
# homeassistant.components.sensor.miflora
miflora==0.1.16

# homeassistant.components.upnp
miniupnpc==1.9

# homeassistant.components.tts
mutagen==1.37.0

Expand Down