Skip to content

Commit 620e5c8

Browse files
committed
Added support for energy usage entities.
1 parent dbfd3cd commit 620e5c8

16 files changed

Lines changed: 389 additions & 46 deletions

README.md

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The supreme versatility and user friendliness of the Crownstones is reflected in
1515
* Extremely easy to set up
1616
* Instant updates for switch states and data
1717
* Dynamically adding and removing devices/entities
18-
* Real-time power usage measurements using the Crownstone USB
18+
* Real-time power and energy usage measurements using the Crownstone USB
1919
* Keeps track of **who** is in which room!
2020
* Custom advanced presence triggers to make your whole home react to your presence!
2121

@@ -27,9 +27,11 @@ This integration stays in one line with the Crownstone app. When data is updated
2727

2828
## HACS
2929

30-
Visit the [HACS installation page](https://hacs.xyz/docs/installation/manual) to set up HACS in your Home Assistant.
30+
Visit the [HACS installation page](https://hacs.xyz/docs/installation/installation) to install HACS, and the [HACS setup page](https://hacs.xyz/docs/configuration/basic) to enabled HACS in Home Assistant.
3131

32-
Click the 3 dots button in the top right corner, and click custom repositories. Select category integration, and add the link of this repository. In the HACS store, click the "+" button in the bottom right corner, and search for Crownstone.
32+
In HACS, Click the 3 dots button in the top right corner, and click custom repositories. Select category integration, and add the link of this repository.
33+
34+
After adding the repository, in the HACS store, click the "+ explore & add repositories" button in the bottom right corner, and search for Crownstone. Then just follow the installation steps. Make sure to select the newest version!
3335

3436
## Manually
3537

@@ -86,25 +88,35 @@ The events are registered, which means that if a user enters a room, but leaves
8688

8789
<img src="images/device_triggers.jpg" width="216" height="468" /> <img src="images/trigger_config.jpg" width="216" height="468" />
8890

89-
# Power usage
91+
# Power usage & Energy usage
92+
93+
Crownstone's live power usage streaming, and energy usage summation, are is also available in Home Assistant. Because of the constant updates, this functionality is only available when using the [Crownstone USB dongle](#crownstone-usb-dongle).
9094

91-
Crownstone's live power usage streaming is also available in Home Assistant. Because of the constant updates, this functionality is only available when using the [Crownstone USB dongle](#crownstone-usb-dongle).
95+
The power usage and energy usage for each Crownstone update every minute, or instantly for a particular Crownstone when switching it.
9296

93-
The power usage for each Crownstone updates every minute, or instantly for a particular Crownstone when switching it. It can take some time before the correct power usage is displayed.
97+
## Energy usage features
9498

95-
## Power usage device triggers
99+
- Starts at 0 in Home Assistant after installing/updating the integration, independent of the value received by the Crownstone
100+
- Persistent states: after rebooting Home Assistant, the energy usage values remain
101+
- When a Crownstone is reset (updated or power loss), the values in Home Assistant will remain, and will be further updated
102+
- The values for energy usage are set back to 0 each month in Home Assistant. In the `History` tab, you will be able to see the maximum usage for each month
103+
104+
## Power usage & Energy usage device triggers
96105

97106
Power usage entities use the default device triggers from sensor for power usage sensors. The following triggers are available:
98-
- Crownstone Power usage energy changes
107+
- Crownstone Power Usage power changes
108+
109+
Energy usage entities also use the default device triggers from sensor for energy usage sensors. The following triggers are available:
110+
- Crownstone Energy Usage energy changes
99111

100-
For `Crownstone Power usage changes` there are 3 options:
112+
For `Crownstone Power Usage power changes` or `Crownstone Energy Usage energy changes` there are 3 options:
101113
- Above a certain value
102114
- Below a certain value
103115
- Duration of the change in hh/mm/ss
104116

105-
You can have other devices react to peaks in power usage, send an event or notification, whatever you like!
117+
Example usage is to send a notification to your phone when the power usage is too high for a certain duration, or switch the device off when this happens. When energy usage reaches a certain value, you could activate a scene which turns multiple devices off.
106118

107-
![Crownstone power usage](/images/power_usage.png)
119+
![Crownstone power usage](/images/power_energy_usage.png)
108120

109121
# Comparison
110122

@@ -121,7 +133,7 @@ Presence updates and data updates are always done using the Crownstone Cloud.
121133
- [ ] No delay when switching Crownstones
122134
- [ ] State updates in Home Assistant when using lightswitch with Switchcraft
123135
- [ ] Can switch Crownstones independently (no smartphone in proximity required)
124-
- [ ] Can use power usage entities
136+
- [ ] Can use power usage & energy usage entities
125137

126138
## Crownstone USB Dongle
127139

@@ -132,7 +144,7 @@ Presence updates and data updates are always done using the Crownstone Cloud.
132144
- [x] No delay when switching Crownstones
133145
- [x] State updates in Home Assistant when using lightswitch with Switchcraft
134146
- [x] Can switch Crownstones independently (no smartphone in proximity required)
135-
- [x] Can use power usage entities
147+
- [x] Can use power usage & energy usage entities
136148

137149
Get your Crownstone USB dongle [here](https://shop.crownstone.rocks/products/crownstone-usb-dongle) and enhance your Home Assistant experience!
138150

@@ -151,9 +163,8 @@ If you like to contribute test results of tests that have not been done by us ye
151163
- [x] Add power usage entities to Crownstone devices
152164
- [x] Fix state updates coming from the Crownstone app not being done in Home Assistant
153165
- [x] Dynamically update data & add/remove Crownstone and Location devices without restarting or reloading
166+
- [x] Add energy usage entities to Crownstone devices
154167
- [ ] Create device conditions for Presence devices
155-
- [ ] Optimize power usage accuracy by implementing new UART protocol
156-
- [ ] Add energy usage entities to Crownstone devices
157168

158169
Any ideas for future updates? Let us [know](mailto:ask@crownstone.rocks?subject=[GitHub]%20Crownstone%20Home%20Assistant%20Integration)!
159170

custom_components/crownstone/config_flow.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self):
2828
"""Initialize the flow."""
2929
self.cloud: Optional[CrownstoneCloud] = None
3030
self.login_info = None
31+
self.sphere_input = None
3132
self.spheres = []
3233

3334
async def async_step_user(self, user_input=None):
@@ -96,8 +97,17 @@ async def async_step_sphere(self, user_input=None):
9697
data_schema=vol.Schema({CONF_SPHERE: vol.In(self.spheres)}),
9798
)
9899

100+
self.sphere_input = user_input
101+
102+
return await self.async_step_usb()
103+
104+
async def async_step_usb(self, user_input=None):
105+
"""Ask a user to plug in a Crowsntone USB (if any)."""
106+
if user_input is None:
107+
return self.async_show_form(step_id="usb")
108+
99109
# set the unique id
100-
await self.async_set_unique_id(user_input[CONF_SPHERE])
110+
await self.async_set_unique_id(self.sphere_input[CONF_SPHERE])
101111

102112
# return data to main
103113
return self.async_create_entry(
@@ -106,6 +116,6 @@ async def async_step_sphere(self, user_input=None):
106116
CONF_ID: self.unique_id,
107117
CONF_EMAIL: self.login_info[CONF_EMAIL],
108118
CONF_PASSWORD: self.login_info[CONF_PASSWORD],
109-
CONF_SPHERE: user_input[CONF_SPHERE],
119+
CONF_SPHERE: self.sphere_input[CONF_SPHERE],
110120
},
111121
)

custom_components/crownstone/const.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Unique ID suffixes
99
CROWNSTONE_SUFFIX = "relay_and_dimmer"
1010
POWER_USAGE_SUFFIX = "power_usage"
11+
ENERGY_USAGE_SUFFIX = "energy_usage"
1112
PRESENCE_SUFFIX = "presence"
1213

1314
# Signals (within integration)
@@ -17,6 +18,8 @@
1718
SIG_CROWNSTONE_UPDATE = "crownstone.crownstone_update"
1819
SIG_POWER_STATE_UPDATE = "crownstone.power_state_update"
1920
SIG_POWER_UPDATE = "crownstone.power_update"
21+
SIG_ENERGY_STATE_UPDATE = "crownstone.energy_state_update"
22+
SIG_ENERGY_UPDATE = "crownstone.energy_update"
2023
SIG_TRIGGER_EVENT = "crownstone.trigger_event"
2124
SIG_ADD_CROWNSTONE_DEVICES = "crownstone.add_crownstone_device"
2225
SIG_ADD_PRESENCE_DEVICES = "crownstone.add_presence_device"
@@ -50,6 +53,11 @@
5053
"description": "Location Presence",
5154
}
5255

56+
# Energy usage constants
57+
JOULE_TO_WH = 3600
58+
MEASUREMENT_START = "Measurement start"
59+
LAST_UPDATE = "Last update"
60+
5361
# Device automation
5462

5563
# Config

custom_components/crownstone/data_updater.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,22 @@
4545
SIG_ADD_PRESENCE_DEVICES,
4646
SIG_CROWNSTONE_STATE_UPDATE,
4747
SIG_CROWNSTONE_UPDATE,
48+
SIG_ENERGY_STATE_UPDATE,
49+
SIG_ENERGY_UPDATE,
4850
SIG_POWER_STATE_UPDATE,
4951
SIG_POWER_UPDATE,
5052
SIG_PRESENCE_STATE_UPDATE,
5153
SIG_PRESENCE_UPDATE,
5254
SIG_TRIGGER_EVENT,
5355
SIG_UART_READY,
5456
)
55-
from .helpers import async_remove_devices, check_items
57+
from .helpers import (
58+
EnergyData,
59+
async_remove_devices,
60+
check_items,
61+
create_utc_timestamp,
62+
process_energy_update,
63+
)
5664

5765
_LOGGER = logging.getLogger(__name__)
5866

@@ -89,6 +97,7 @@ def __init__(self, hass, entry, user_data, sse):
8997
UartEventBus.subscribe(SystemTopics.connectionClosed, self.update_uart_state)
9098
UartEventBus.subscribe(UartTopics.newDataAvailable, self.update_crwn_state_uart)
9199
UartEventBus.subscribe(UartTopics.newDataAvailable, self.update_power_usage)
100+
UartEventBus.subscribe(UartTopics.newDataAvailable, self.update_energy_usage)
92101

93102
# SSE UPDATES
94103

@@ -220,6 +229,10 @@ async def update_data(self, data_event: DataChangeEvent) -> None:
220229
async_dispatcher_send(
221230
self.hass, SIG_POWER_UPDATE, data_event.changed_item_id
222231
)
232+
# update energy usage entity (name)
233+
async_dispatcher_send(
234+
self.hass, SIG_ENERGY_UPDATE, data_event.changed_item_id
235+
)
223236

224237
# additions or deletions
225238
if data_event.operation in (OPERATION_CREATE, OPERATION_DELETE):
@@ -303,12 +316,28 @@ def update_power_usage(self, data) -> None:
303316
"""Update the power usage of a Crownstone when a Crownstone USB is available."""
304317
update_crownstone = self.user_data.crownstones.find_by_uid(data["id"])
305318
if update_crownstone is not None:
306-
# for now, make sure power usage can't go below zero
307-
# an improved version of power usage measuring is in development
308319
if data["powerUsageReal"] < 0:
309320
update_crownstone.power_usage = 0
310321
else:
311322
update_crownstone.power_usage = int(data["powerUsageReal"])
312323

313324
# update HA state
314325
async_dispatcher_send(self.hass, SIG_POWER_STATE_UPDATE)
326+
327+
def update_energy_usage(self, data) -> None:
328+
"""Update the energy usage of a Crownstone when a Crownstone USB is available."""
329+
update_crownstone = self.user_data.crownstones.find_by_uid(data["id"])
330+
if update_crownstone is not None:
331+
# create object that holds energy usage variables
332+
new_energy_usage = EnergyData(
333+
data["accumulatedEnergy"], create_utc_timestamp(data["timestamp"])
334+
)
335+
336+
# compare new values to existing ones
337+
process_energy_update(new_energy_usage, update_crownstone.energy_usage)
338+
339+
# set new data point
340+
update_crownstone.energy_usage = new_energy_usage
341+
342+
# update HA state
343+
async_dispatcher_send(self.hass, SIG_ENERGY_STATE_UPDATE)

custom_components/crownstone/device_trigger.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
CONF_PLATFORM,
1717
CONF_TYPE,
1818
DEVICE_CLASS_ENERGY,
19+
DEVICE_CLASS_POWER,
1920
)
2021
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
2122
from homeassistant.helpers import config_validation as cv, entity_registry
@@ -107,7 +108,11 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
107108
# loop through all entities for device
108109
for entry in entity_registry.async_entries_for_device(registry, device_id):
109110
# make sure to only add custom triggers to presence sensor entities
110-
if entry.domain != SENSOR_PLATFORM or entry.device_class == DEVICE_CLASS_ENERGY:
111+
if (
112+
entry.domain != SENSOR_PLATFORM
113+
or entry.device_class == DEVICE_CLASS_ENERGY
114+
or entry.device_class == DEVICE_CLASS_POWER
115+
):
111116
continue
112117

113118
for trigger in TRIGGER_TYPES:

custom_components/crownstone/helpers.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Helper functions/classes for Crownstone."""
22
import asyncio
3+
from datetime import datetime, timezone
34
import threading
5+
from typing import Any
46

57
from crownstone_uart import CrownstoneUart
8+
from tzlocal import get_localzone
69

710
from homeassistant.config_entries import ConfigEntry
811
from homeassistant.core import HomeAssistant
@@ -38,6 +41,23 @@ def stop(self) -> None:
3841
self.uart_instance.stop()
3942

4043

44+
class EnergyData:
45+
"""Data class that holds energy measurements."""
46+
47+
def __init__(self, accumulated_energy: int, utc_timestamp: Any) -> None:
48+
"""Initialize the object."""
49+
# new value obtained from UART
50+
self.energy_usage = accumulated_energy
51+
# the new energy usage value (after adding the offset or previous value)
52+
self.corrected_energy_usage = 0
53+
# timestamp of the measurement in UTC
54+
self.timestamp = utc_timestamp
55+
# flag for being a first node in the energy data chain
56+
self.first_measurement = False
57+
# flag for a restored state from previous session
58+
self.restored_state = False
59+
60+
4161
def set_to_dict(input_set: set):
4262
"""Convert a set to a dictionary."""
4363
return {key: key for key in input_set}
@@ -81,3 +101,72 @@ async def async_remove_devices(
81101
device_reg.async_update_device(
82102
device.id, remove_config_entry_id=entry.entry_id
83103
)
104+
105+
106+
def create_utc_timestamp(cs_timestamp: int):
107+
"""Create a UTC timestamp from a localzone Crownstone timestamp."""
108+
# get the timezone of this computer
109+
tz = get_localzone()
110+
date = datetime.fromtimestamp(cs_timestamp, tz)
111+
# calculate the offset
112+
utc_offset = date.utcoffset().total_seconds()
113+
# utc is the positive east, calculate timestamp
114+
return cs_timestamp - utc_offset
115+
116+
117+
def process_energy_update(
118+
next_data_point: EnergyData, previous_data_point: EnergyData
119+
) -> None:
120+
"""
121+
Process an update for the energy usage.
122+
123+
It's possible for devices to reboot (power loss, sw update, crash).
124+
After a reboot the saved value for energy goes back to zero.
125+
A check is done to prevent the value in HA from resetting as well.
126+
"""
127+
next_value = next_data_point.energy_usage
128+
next_timestamp = next_data_point.timestamp
129+
previous_raw_value = previous_data_point.energy_usage
130+
previous_value = previous_data_point.corrected_energy_usage
131+
previous_timestamp = previous_data_point.timestamp
132+
133+
# create data objects from timestamps
134+
# check if a month or year is past
135+
# we set the energy usage back to 0 each month
136+
if previous_timestamp is not None:
137+
next_date = datetime.fromtimestamp(next_timestamp, timezone.utc)
138+
previous_date = datetime.fromtimestamp(previous_timestamp, timezone.utc)
139+
140+
if next_date.year > previous_date.year or next_date.month > previous_date.month:
141+
next_data_point.corrected_energy_usage = 0
142+
next_data_point.first_measurement = True
143+
return
144+
145+
# initial HA value, make sure we start at 0 in HA
146+
# set first measurement flag to save the start date
147+
if previous_raw_value == 0 and previous_timestamp is None:
148+
next_value = 0
149+
next_data_point.first_measurement = True
150+
151+
# restored data point, set new measurement to saved value
152+
elif previous_data_point.restored_state:
153+
next_value = previous_value
154+
155+
else:
156+
# calculate offset value
157+
offset_value = previous_value - previous_raw_value
158+
159+
# if the new value is greater than the offset value, accept measurement
160+
# if it is smaller, add the new value to the offsetvalue
161+
# check if the new value is below the old value, but just a little, since it can increase fast
162+
if next_value < previous_raw_value * 0.9:
163+
next_value += previous_value
164+
else:
165+
next_value += offset_value
166+
167+
# ignore change if next value is still smaller, energy decrease unsupported
168+
if next_value < previous_value:
169+
next_value = previous_value
170+
171+
# set new value
172+
next_data_point.corrected_energy_usage = next_value

custom_components/crownstone/hub.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,9 @@ async def async_setup(self) -> bool:
7575
self.uart_manager.start()
7676

7777
# Create SSE instance
78-
# NOTE: hass loop is currently used as parameter,
79-
# to run coroutines within the CrownstoneSSE eventbus in the HA loop.
80-
# this is because CrownstoneSSE runs in an other OS thread.
81-
self.sse = CrownstoneSSE(customer_email, customer_password, self.hass.loop)
78+
self.sse = CrownstoneSSE(
79+
customer_email, customer_password, asyncio.get_running_loop()
80+
)
8281
self.sse.set_access_token(self.cloud.access_token)
8382
self.sse.start()
8483

0 commit comments

Comments
 (0)