Skip to content

Commit

Permalink
Update power consumption service #124
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed May 27, 2020
1 parent ef0f05d commit b1d0a4a
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 26 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Home Assistant custom component for control [Sonoff](https://www.itead.cc/) devi
- support new device types: color lights, sensors, covers
- support eWeLink cameras with PTZ ([read more](#sonoff-gk-200mp2-b-camera))
- support unavailable device state for both local and cloud connection
- support refresh interval for Sonoff TH and Sonoff POW ([read more](#refresh-interval-for-th-and-pow))
- support refresh interval for Sonoff TH and Sonoff Pow ([read more](#refresh-interval-for-th-and-pow))
- added new debug mode for troubleshooting ([read more](#component-debug-mode))

**Breaking changes 2.0:** by default, both local and cloud modes will start working together. If you do not want this - enable the `mode: local` setting. But I recommend using the new mode, it works great.
Expand All @@ -31,13 +31,13 @@ Pros:
- work with devices without DIY-mode
- work with devices in DIY-mode ([read more](#local-only-mode-diy-devices))
- support single and multi-channel devices
- support TH and POW device attributes ([read more](#sonoff-th-and-pow))
- support TH and Pow device attributes ([read more](#sonoff-th-and-pow))
- support Sonoff RF Bridge 433 for receive and send commands ([read more](#sonoff-rf-bridge-433))
- support Sonoff GK-200MP2-B Camera ([read more](#sonoff-gk-200mp2-b-camera))
- instant device state update with Local Multicast or Cloud Websocket connection
- load devices list from eWeLink Servers (with names, apikey/devicekey and device_class) and save it locally
- (optional) change device type from `switch` to `light` ([read more](#custom-device_class-for-any-mode))
- (optional) config force refresh interval for TH and POW ([read more](#refresh-interval-for-th-and-pow))
- (optional) config force refresh interval for TH and Pow ([read more](#refresh-interval-for-th-and-pow))

**Component review from DrZzs (HOWTO about HACS)**

Expand Down Expand Up @@ -278,9 +278,9 @@ sonoff:
device_class: exclude
```

### Refresh interval for TH and POW
### Refresh interval for TH and Pow

You can config forced updating of TH and POW attributes ([read more](https://github.com/AlexxIT/SonoffLAN/issues/14)).
You can config forced updating of TH and Pow attributes ([read more](https://github.com/AlexxIT/SonoffLAN/issues/14)).

```yaml
sonoff:
Expand All @@ -296,6 +296,17 @@ sonoff:
force_update: True
```

## Sonoff Pow Power Consumption

For update power consumption of all your Pow devices you can call `sonoff.update_consumption` service.

The device attributes will display data for the last 100 days. The first element is today's data. It's up to you how often to call updates and what to do with this data later.

```jinja2
Today consumpion: {{ state_attr('switch.sonoff_1000abcdef', 'consumption')[0] }}
10 days consumpion: {{ state_attr('switch.sonoff_1000abcdef', 'consumption')[:10]|sum }}
```

## Sonoff RF Bridge 433

**Video HOWTO from @KPeyanski**
Expand Down
10 changes: 10 additions & 0 deletions custom_components/sonoff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from . import utils
from .sonoff_camera import EWeLinkCameras
from .sonoff_cloud import ConsumptionHelper
from .sonoff_main import EWeLinkRegistry

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -171,6 +172,15 @@ async def send_command(call: ServiceCall):

hass.services.async_register(DOMAIN, 'send_command', send_command)

async def update_consumption(call: ServiceCall):
if not hasattr(registry, 'consumption'):
_LOGGER.debug("Create ConsumptionHelper")
registry.consumption = ConsumptionHelper(registry.cloud)
await registry.consumption.update()

hass.services.async_register(DOMAIN, 'update_consumption',
update_consumption)

if CONF_SCAN_INTERVAL in config:
global SCAN_INTERVAL
SCAN_INTERVAL = config[CONF_SCAN_INTERVAL]
Expand Down
3 changes: 3 additions & 0 deletions custom_components/sonoff/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ send_command:
cmd:
description: A single command to send.
example: 'switch'

update_consumption:
description: Power consumption update of all Pow devices.
38 changes: 32 additions & 6 deletions custom_components/sonoff/sonoff_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"Read more: https://github.com/AlexxIT/SonoffLAN#config-examples")


class ResponseFuture:
class ResponseWaiter:
"""Class wait right sequences in response messages."""
_waiters = {}

Expand All @@ -55,8 +55,8 @@ async def _wait_response(self, sequence: str, timeout: int = 5):
return self._waiters.pop(sequence).result()


class EWeLinkCloud(ResponseFuture):
_devices: dict = None
class EWeLinkCloud(ResponseWaiter):
devices: dict = None
_handlers = None
_ws: Optional[ClientWebSocketResponse] = None

Expand Down Expand Up @@ -113,7 +113,8 @@ async def _process_ws_msg(self, data: dict):
# if msg about device
if deviceid:
_LOGGER.debug(f"{deviceid} <= Cloud3 | {data}")
device = self._devices[deviceid]

device = self.devices[deviceid]

# if msg with device params
if 'params' in data:
Expand Down Expand Up @@ -255,7 +256,7 @@ def started(self) -> bool:
async def start(self, handlers: List[Callable], devices: dict = None):
assert self._token, "Login first"
self._handlers = handlers
self._devices = devices
self.devices = devices

asyncio.create_task(self._connect())

Expand All @@ -269,7 +270,7 @@ async def send(self, deviceid: str, data: dict, sequence: str):
payload = {
'action': 'update',
# device apikey for shared devices
'apikey': self._devices[deviceid]['apikey'],
'apikey': self.devices[deviceid]['apikey'],
'selfApikey': self._apikey,
'deviceid': deviceid,
'userAgent': 'app',
Expand All @@ -282,3 +283,28 @@ async def send(self, deviceid: str, data: dict, sequence: str):

# wait for response with same sequence
return await self._wait_response(sequence)


class ConsumptionHelper:
def __init__(self, cloud: EWeLinkCloud):
self.cloud = cloud
self._cloud_process_ws_msg = cloud._process_ws_msg
cloud._process_ws_msg = self._process_ws_msg

async def _process_ws_msg(self, data: dict):
if 'config' in data and 'hundredDaysKwhData' in data['config']:
kwh = data['config']['hundredDaysKwhData']
kwh = [int(kwh[i:i + 6]) for i in range(0, 600, 6)]
data['params'] = {'consumption': kwh}

await self._cloud_process_ws_msg(data)

async def update(self):
if not self.cloud.started:
return

for device in self.cloud.devices.values():
if 'params' in device and 'hundredDaysKwh' in device['params']:
sequence = str(int(time.time() * 1000))
await self.cloud.send(device['deviceid'], {
'hundredDaysKwh': 'get'}, sequence)
30 changes: 15 additions & 15 deletions custom_components/sonoff/sonoff_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
_LOGGER = logging.getLogger(__name__)

ATTRS = ('local', 'cloud', 'rssi', 'humidity', 'temperature', 'power',
'current', 'voltage', 'battery')
'current', 'voltage', 'battery', 'consumption')

# map cloud attrs to local attrs
ATTRS_MAP = {
Expand Down Expand Up @@ -62,8 +62,8 @@ class EWeLinkRegistry:
devices: Optional[dict] = None

def __init__(self, session: ClientSession):
self._cloud = EWeLinkCloud(session)
self._local = EWeLinkLocal(session)
self.cloud = EWeLinkCloud(session)
self.local = EWeLinkLocal(session)

def _registry_handler(self, deviceid: str, state: dict, sequence: str):
"""Feedback from local and cloud connections
Expand Down Expand Up @@ -111,11 +111,11 @@ def cache_load_devices(self, cachefile: str):
self.devices = load_cache(cachefile)

async def cloud_login(self, username: str, password: str):
return await self._cloud.login(username, password)
return await self.cloud.login(username, password)

async def cloud_load_devices(self, cachefile: str = None):
"""Load devices list from Cloud Servers."""
newdevices = await self._cloud.load_devices()
newdevices = await self.cloud.load_devices()
if newdevices is not None:
newdevices = {p['deviceid']: p for p in newdevices}
if cachefile:
Expand All @@ -126,7 +126,7 @@ async def cloud_start(self):
if self.devices is None:
self.devices = {}

await self._cloud.start([self._registry_handler], self.devices)
await self.cloud.start([self._registry_handler], self.devices)

async def local_start(self, handlers: List[Callable]):
if self.devices is None:
Expand All @@ -137,7 +137,7 @@ async def local_start(self, handlers: List[Callable]):
else:
handlers = [self._registry_handler]

self._local.start(handlers, self.devices)
self.local.start(handlers, self.devices)

async def stop(self):
# TODO: do something
Expand All @@ -148,30 +148,30 @@ async def send(self, deviceid: str, params: dict):
seq = str(int(time.time() * 1000))

device: dict = self.devices[deviceid]
can_local = self._local.started and device.get('host')
can_cloud = self._cloud.started and device.get('online')
can_local = self.local.started and device.get('host')
can_cloud = self.cloud.started and device.get('online')

state = {}

if can_local and can_cloud:
# try to send a command locally (wait no more than a second)
state['local'] = await self._local.send(deviceid, params, seq, 1)
state['local'] = await self.local.send(deviceid, params, seq, 1)

# otherwise send a command through the cloud
if state['local'] != 'online':
state['cloud'] = await self._cloud.send(deviceid, params, seq)
state['cloud'] = await self.cloud.send(deviceid, params, seq)
if state['cloud'] != 'online':
coro = self._local.check_offline(deviceid)
coro = self.local.check_offline(deviceid)
asyncio.create_task(coro)

elif can_local:
state['local'] = await self._local.send(deviceid, params, seq, 5)
state['local'] = await self.local.send(deviceid, params, seq, 5)
if state['local'] != 'online':
coro = self._local.check_offline(deviceid)
coro = self.local.check_offline(deviceid)
asyncio.create_task(coro)

elif can_cloud:
state['cloud'] = await self._cloud.send(deviceid, params, seq)
state['cloud'] = await self.cloud.send(deviceid, params, seq)

else:
return
Expand Down

0 comments on commit b1d0a4a

Please sign in to comment.