Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
d15d223
Added support for REST sensors
chemelli74 Sep 21, 2020
413f4f3
Added icon for Uptime aensor
chemelli74 Sep 21, 2020
5bfb099
RSSI disabled by default + REST sensors as part of main device
chemelli74 Sep 21, 2020
c7cf0a0
Fix sensor name case
chemelli74 Sep 22, 2020
6a17bc1
Removed icon when using DEVICE_CLASS
chemelli74 Sep 22, 2020
d3247cb
Cleanup leftover
chemelli74 Sep 22, 2020
9095ca9
Fix for sensors not updating
chemelli74 Sep 22, 2020
a8db66e
Fix for sensors not updating #2
chemelli74 Sep 23, 2020
2f6f81f
Added current firmware attribute
chemelli74 Sep 25, 2020
07f0a37
Code cleanup
chemelli74 Sep 25, 2020
2a041e1
Update after #40119
chemelli74 Sep 27, 2020
d648cc0
Added support for REST sensors
chemelli74 Sep 21, 2020
b1e5358
Added icon for Uptime aensor
chemelli74 Sep 21, 2020
7effad4
Removed icon when using DEVICE_CLASS
chemelli74 Sep 22, 2020
ee7000c
Cleanup leftover
chemelli74 Sep 22, 2020
6a6e98a
Fix for sensors not updating
chemelli74 Sep 22, 2020
bf6c44f
Fix for sensors not updating #2
chemelli74 Sep 23, 2020
a691a0d
Implemented code suggestions
chemelli74 Sep 27, 2020
e3ad1ca
Added support for REST sensors
chemelli74 Sep 21, 2020
583dec5
Added icon for Uptime aensor
chemelli74 Sep 21, 2020
3d4ab3e
RSSI disabled by default + REST sensors as part of main device
chemelli74 Sep 21, 2020
01e1096
Removed icon when using DEVICE_CLASS
chemelli74 Sep 22, 2020
ed5e54e
Fix for sensors not updating #2
chemelli74 Sep 23, 2020
e7fbe85
Applied suggestions for firmware sensor
chemelli74 Sep 30, 2020
d3552da
Update after #40436
chemelli74 Oct 1, 2020
564739c
Added support for REST sensors
chemelli74 Sep 21, 2020
f61273c
Added icon for Uptime aensor
chemelli74 Sep 21, 2020
42d1e90
Removed icon when using DEVICE_CLASS
chemelli74 Sep 22, 2020
5882358
Fix for sensors not updating
chemelli74 Sep 22, 2020
9b40132
Fix for sensors not updating #2
chemelli74 Sep 23, 2020
f0d2669
Added current firmware attribute
chemelli74 Sep 25, 2020
6d20de6
Code cleanup
chemelli74 Sep 25, 2020
d0b0c2c
Update after #40119
chemelli74 Sep 27, 2020
4ae6423
Added support for REST sensors
chemelli74 Sep 21, 2020
2e9dd77
Added icon for Uptime aensor
chemelli74 Sep 21, 2020
77f4325
Removed icon when using DEVICE_CLASS
chemelli74 Sep 22, 2020
5a42ba9
Cleanup leftover
chemelli74 Sep 22, 2020
81f74d1
Fix for sensors not updating
chemelli74 Sep 22, 2020
0a76498
Fix for sensors not updating #2
chemelli74 Sep 23, 2020
d76d2d1
Implemented code suggestions
chemelli74 Sep 27, 2020
bcf87eb
Added support for REST sensors
chemelli74 Sep 21, 2020
73e2548
Added icon for Uptime aensor
chemelli74 Sep 21, 2020
20489dc
RSSI disabled by default + REST sensors as part of main device
chemelli74 Sep 21, 2020
42f822a
Removed icon when using DEVICE_CLASS
chemelli74 Sep 22, 2020
15a73af
Cleanup leftover
chemelli74 Sep 22, 2020
4c32f86
Fix for sensors not updating
chemelli74 Sep 22, 2020
28f3f86
Fix for sensors not updating #2
chemelli74 Sep 23, 2020
bc024f4
Applied suggestions for firmware sensor
chemelli74 Sep 30, 2020
ee19df9
Rebase and cleanup
chemelli74 Nov 5, 2020
e998dd5
Added rest wrapper and refresh (thx to @Bieniu)
chemelli74 Nov 7, 2020
17b9516
Moved parser func to utils
chemelli74 Nov 8, 2020
4624514
Param optimization
chemelli74 Nov 8, 2020
5c3dc69
Missing import
chemelli74 Nov 8, 2020
e80aafd
Applied suggestions
chemelli74 Nov 9, 2020
1427dbf
Applied rename suggestion
chemelli74 Nov 9, 2020
73bd655
Moved REST update interval to const
chemelli74 Nov 9, 2020
f6ef3d1
Temporary excluding battery devices
chemelli74 Nov 9, 2020
692bf0f
Apply code review suggestions
chemelli74 Nov 11, 2020
bf32003
Fix rebase errors
chemelli74 Nov 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions homeassistant/components/shelly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
)

from .const import (
COAP,
DATA_CONFIG_ENTRY,
DOMAIN,
POLLING_TIMEOUT_MULTIPLIER,
REST,
REST_SENSORS_UPDATE_INTERVAL,
SETUP_ENTRY_TIMEOUT_SEC,
SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER,
Expand Down Expand Up @@ -82,10 +85,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
except (asyncio.TimeoutError, OSError) as err:
raise ConfigEntryNotReady from err

wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
entry.entry_id
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
COAP
] = ShellyDeviceWrapper(hass, entry, device)
await wrapper.async_setup()
await coap_wrapper.async_setup()

hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
REST
] = ShellyDeviceRestWrapper(hass, device)

for component in PLATFORMS:
hass.async_create_task(
Expand Down Expand Up @@ -169,6 +177,37 @@ def shutdown(self):
self.device.shutdown()


class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
"""Rest Wrapper for a Shelly device with Home Assistant specific functions."""

def __init__(self, hass, device: aioshelly.Device):
"""Initialize the Shelly device wrapper."""

super().__init__(
hass,
_LOGGER,
name=device.settings["name"] or device.settings["device"]["hostname"],
update_interval=timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL),
)
self.device = device

async def _async_update_data(self):
"""Fetch data."""
try:
async with async_timeout.timeout(5):
_LOGGER.debug(
"REST update for %s", self.device.settings["device"]["hostname"]
)
return await self.device.update_status()
except OSError as err:
raise update_coordinator.UpdateFailed("Error fetching data") from err

@property
def mac(self):
"""Mac address of the device."""
return self.device.settings["device"]["mac"]


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
Expand All @@ -180,6 +219,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
)
)
if unload_ok:
hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id).shutdown()
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][COAP].shutdown()
hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id)

return unload_ok
33 changes: 33 additions & 0 deletions homeassistant/components/shelly/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Binary sensor for Shelly."""
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_OPENING,
Expand All @@ -11,8 +12,11 @@

from .entity import (
BlockAttributeDescription,
RestAttributeDescription,
ShellyBlockAttributeEntity,
ShellyRestAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_rest,
)

SENSORS = {
Expand Down Expand Up @@ -48,13 +52,33 @@
),
}

REST_SENSORS = {
"cloud": RestAttributeDescription(
name="Cloud",
device_class=DEVICE_CLASS_CONNECTIVITY,
default_enabled=False,
path="cloud/connected",
),
"fwupdate": RestAttributeDescription(
name="Firmware update",
icon="mdi:update",
default_enabled=False,
path="update/has_update",
attributes={"description": "available version:", "path": "update/new_version"},
),
}


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up sensors for device."""
await async_setup_entry_attribute_entities(
hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor
)

await async_setup_entry_rest(
hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestBinarySensor
)


class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
"""Shelly binary sensor entity."""
Expand All @@ -63,3 +87,12 @@ class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
def is_on(self):
"""Return true if sensor state is on."""
return bool(self.attribute_value)


class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity):
"""Shelly REST binary sensor entity."""

@property
def is_on(self):
"""Return true if REST sensor state is on."""
return bool(self.attribute_value)
5 changes: 5 additions & 0 deletions homeassistant/components/shelly/const.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
"""Constants for the Shelly integration."""

COAP = "coap"
DATA_CONFIG_ENTRY = "config_entry"
DOMAIN = "shelly"
REST = "rest"

# Used to calculate the timeout in "_async_update_data" used for polling data from devices.
POLLING_TIMEOUT_MULTIPLIER = 1.2

# Refresh interval for REST sensors
REST_SENSORS_UPDATE_INTERVAL = 60

# Timeout used for initial entry setup in "async_setup_entry".
SETUP_ENTRY_TIMEOUT_SEC = 10

Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/shelly/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
from homeassistant.core import callback

from . import ShellyDeviceWrapper
from .const import DATA_CONFIG_ENTRY, DOMAIN
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN
from .entity import ShellyBlockEntity


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up cover for device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id]
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
blocks = [block for block in wrapper.device.blocks if block.type == "roller"]

if not blocks:
Expand Down
180 changes: 173 additions & 7 deletions homeassistant/components/shelly/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,60 @@

import aioshelly

from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback
from homeassistant.helpers import device_registry, entity

from . import ShellyDeviceWrapper
from .const import DATA_CONFIG_ENTRY, DOMAIN
from .utils import get_entity_name
from homeassistant.helpers import device_registry, entity, update_coordinator

from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
from .utils import get_entity_name, get_rest_value_from_path


def temperature_unit(block_info: dict) -> str:
"""Detect temperature unit."""
if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F":
return TEMP_FAHRENHEIT
return TEMP_CELSIUS


def shelly_naming(self, block, entity_type: str):
"""Naming for switch and sensors."""

entity_name = self.wrapper.name
if not block:
return f"{entity_name} {self.description.name}"

channels = 0
mode = block.type + "s"
if "num_outputs" in self.wrapper.device.shelly:
channels = self.wrapper.device.shelly["num_outputs"]
if (
self.wrapper.model in ["SHSW-21", "SHSW-25"]
and self.wrapper.device.settings["mode"] == "roller"
):
channels = 1
if block.type == "emeter" and "num_emeters" in self.wrapper.device.shelly:
channels = self.wrapper.device.shelly["num_emeters"]
if channels > 1 and block.type != "device":
# Shelly EM (SHEM) with firmware v1.8.1 doesn't have "name" key; will be fixed in next firmware release
if "name" in self.wrapper.device.settings[mode][int(block.channel)]:
entity_name = self.wrapper.device.settings[mode][int(block.channel)]["name"]
else:
entity_name = None
if not entity_name:
if self.wrapper.model == "SHEM-3":
base = ord("A")
else:
base = ord("1")
entity_name = f"{self.wrapper.name} channel {chr(int(block.channel)+base)}"

if entity_type == "switch":
return entity_name

if entity_type == "sensor":
return f"{entity_name} {self.description.name}"

raise ValueError


async def async_setup_entry_attribute_entities(
Expand All @@ -18,7 +66,7 @@ async def async_setup_entry_attribute_entities(
"""Set up entities for block attributes."""
wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
]
][COAP]
blocks = []

for block in wrapper.device.blocks:
Expand All @@ -44,6 +92,27 @@ async def async_setup_entry_attribute_entities(
)


async def async_setup_entry_rest(
hass, config_entry, async_add_entities, sensors, sensor_class
):
"""Set up entities for REST sensors."""
wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
][REST]

entities = []
for sensor_id in sensors:
_desc = sensors.get(sensor_id)

if not wrapper.device.settings.get("sleep_mode"):
entities.append(_desc)

if not entities:
return

async_add_entities([sensor_class(wrapper, description) for description in entities])


@dataclass
class BlockAttributeDescription:
"""Class to describe a sensor."""
Expand All @@ -60,6 +129,21 @@ class BlockAttributeDescription:
] = None


@dataclass
class RestAttributeDescription:
"""Class to describe a REST sensor."""

path: str
name: str
# Callable = lambda attr_info: unit
icon: Optional[str] = None
unit: Union[None, str, Callable[[dict], str]] = None
Comment thread
chemelli74 marked this conversation as resolved.
value: Callable[[Any], Any] = lambda val: val
device_class: Optional[str] = None
default_enabled: bool = True
attributes: Optional[dict] = None


class ShellyBlockEntity(entity.Entity):
"""Helper class to represent a block."""

Expand Down Expand Up @@ -133,7 +217,7 @@ def __init__(

self._unit = unit
self._unique_id = f"{super().unique_id}-{self.attribute}"
self._name = get_entity_name(wrapper, block, self.description.name)
self._name = shelly_naming(self, block, "sensor")

@property
def unique_id(self):
Expand Down Expand Up @@ -187,3 +271,85 @@ def device_state_attributes(self):
return None

return self.description.device_state_attributes(self.block)


class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
"""Class to load info from REST."""

def __init__(
self, wrapper: ShellyDeviceWrapper, description: RestAttributeDescription
) -> None:
"""Initialize sensor."""
super().__init__(wrapper)
self.wrapper = wrapper
self.description = description

self._unit = self.description.unit
self._name = shelly_naming(self, None, "sensor")
self.path = self.description.path
self._attributes = self.description.attributes

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

@property
def device_info(self):
"""Device info."""
return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
}

@property
def entity_registry_enabled_default(self) -> bool:
"""Return if it should be enabled by default."""
return self.description.default_enabled

@property
def available(self):
"""Available."""
return self.wrapper.last_update_success

@property
def attribute_value(self):
"""Attribute."""
return get_rest_value_from_path(
self.wrapper.device.status, self.description.device_class, self.path
)

@property
def unit_of_measurement(self):
"""Return unit of sensor."""
return self.description.unit

@property
def device_class(self):
"""Device class of sensor."""
return self.description.device_class

@property
def icon(self):
"""Icon of sensor."""
return self.description.icon

@property
def unique_id(self):
"""Return unique ID of entity."""
return f"{self.wrapper.mac}-{self.description.path}"

@property
def device_state_attributes(self):
"""Return the state attributes."""

if self._attributes is None:
return None

_description = self._attributes.get("description")
_attribute_value = get_rest_value_from_path(
Comment on lines +348 to +349
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.

Why prefix with _ ?

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 though "_" was a code convention for non public func/values. Am I wrong ?

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.

As class methods, yes. When used for variables it means it's not going to be used.

self.wrapper.device.status,
self.description.device_class,
self._attributes.get("path"),
)

return {_description: _attribute_value}
Comment thread
chemelli74 marked this conversation as resolved.
Loading