Skip to content

Commit 48a4a1b

Browse files
committed
Added live measurement tests
1 parent a44d320 commit 48a4a1b

File tree

4 files changed

+79
-27
lines changed

4 files changed

+79
-27
lines changed

tests/realtime/test_live_measurements.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from datetime import timedelta
55

66
import tibber
7+
from tibber.types.live_measurement import LiveMeasurement
78

89

910
@pytest.fixture
@@ -18,4 +19,51 @@ def test_adding_listener_with_unknown_event_raises_exception(home):
1819
with pytest.raises(ValueError):
1920
@home.event("invalid-event-name")
2021
def callback(data):
21-
print(data)
22+
print(data)
23+
24+
def test_starting_live_feed_with_no_listeners_shows_warning(home, caplog):
25+
# Return immediately after the first callback
26+
home.start_live_feed(exit_condition = lambda data: True)
27+
assert "The event that was broadcasted has no listeners / callbacks! Nothing was run." in caplog.text
28+
29+
def test_retrieving_live_measurements(home):
30+
global callback_was_run
31+
callback_was_run = False
32+
@home.event("live_measurement")
33+
def callback(data):
34+
global callback_was_run
35+
callback_was_run = True
36+
assert isinstance(data, LiveMeasurement)
37+
timestamp = datetime.strptime(data.timestamp, "%Y-%m-%dT%H:%M:%S.%f%z")
38+
timestamp = timestamp.replace(tzinfo=None)
39+
now = datetime.now().replace(tzinfo=None)
40+
assert timestamp > now - timedelta(seconds=30)
41+
assert data.power > 0
42+
assert data.last_meter_consumption > 0
43+
assert data.accumulated_consumption > 0
44+
assert isinstance(data.accumulated_production, float)
45+
assert data.accumulated_consumption_last_hour > 0
46+
assert isinstance(data.accumulated_production_last_hour, float)
47+
assert data.accumulated_cost > 0
48+
assert isinstance(data.accumulated_reward, float)
49+
assert data.currency == "SEK"
50+
assert data.min_power > 0
51+
assert data.max_power > 0
52+
assert data.average_power > 0
53+
assert isinstance(data.power_production, float)
54+
assert isinstance(data.power_reactive, float)
55+
assert data.power_production_reactive > 0
56+
assert isinstance(data.min_power_production, float)
57+
assert isinstance(data.max_power_production, float)
58+
assert data.last_meter_production > 0
59+
assert data.power_factor > 0
60+
assert data.voltage_phase_1 > 0
61+
assert data.voltage_phase_2 > 0
62+
assert data.voltage_phase_3 > 0
63+
assert data.current_l1 > 0
64+
assert data.current_l2 > 0
65+
assert data.current_l3 > 0
66+
67+
# Return immediately after the first callback
68+
home.start_live_feed(exit_condition = lambda data: True)
69+
assert callback_was_run

tibber/networking/query_executor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ def __init__(self, websession: Optional[aiohttp.ClientSession] = None):
1919
try:
2020
self.eventloop = asyncio.get_running_loop()
2121
except RuntimeError:
22-
self.logger.debug("No running event loop was found. Creating a new one with asyncio.get_event_loop()")
23-
self.eventloop = asyncio.get_event_loop()
22+
self.logger.debug("No running event loop was found. Creating a new one with asyncio.new_event_loop()")
23+
self.eventloop = asyncio.new_event_loop()
2424

2525
self.eventloop.run_until_complete(self.__async_init__(websession))
2626

tibber/types/home.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,11 @@ class TibberHome(NonDecoratedTibberHome):
180180
"""
181181
def __init__(self, *args, **kwargs):
182182
super().__init__(*args, **kwargs)
183-
self.loop = asyncio.get_event_loop()
184-
self.websocket_client = None # Used to reference the websocket connection later, if it needs to be closed for example.
183+
try:
184+
self._loop = asyncio.get_running_loop()
185+
except RuntimeError:
186+
self._loop = asyncio.new_event_loop()
187+
self._websocket_client = None # Used to reference the websocket connection later, if it needs to be closed for example.
185188
self.websocket_running = False
186189
self._callbacks = {}
187190

@@ -222,17 +225,17 @@ def start_live_feed(self, exit_condition: Callable[[LiveMeasurement], bool] = No
222225
raise ValueError("The home does not have real time consumption enabled.")
223226

224227
try:
225-
self.tibber_client.eventloop.run_until_complete(self.run_websocket_loop(exit_condition, retries = retries, **kwargs))
226-
except KeyboardInterrupt:
228+
self._loop.run_until_complete(self.run_websocket_loop(exit_condition, retries = retries, **kwargs))
229+
except KeyboardInterrupt: # pragma: no cover
227230
print("Closing websocket...")
228-
if self.websocket_client:
231+
if self._websocket_client:
229232
self.websocket_running = False
230-
self.tibber_client.eventloop.run_until_complete(self.websocket_client.close_async())
231-
except BaseException as e:
233+
self._loop.run_until_complete(self._websocket_client.close_async())
234+
except BaseException as e: # pragma: no cover
232235
# Close the websocket, then re-raise the exception
233-
if self.websocket_client:
236+
if self._websocket_client:
234237
self.websocket_running = False
235-
self.tibber_client.eventloop.run_until_complete(self.websocket_client.close_async())
238+
self._loop.run_until_complete(self._websocket_client.close_async())
236239
raise e
237240

238241

@@ -254,7 +257,7 @@ async def retrieve_from_websocket():
254257
headers={"Authorization": self.tibber_client.token}
255258
)
256259

257-
self.websocket_client = gql.Client(
260+
self._websocket_client = gql.Client(
258261
transport=transport,
259262
fetch_schema_from_transport=True,
260263
)
@@ -264,7 +267,7 @@ async def retrieve_from_websocket():
264267
self.logger.debug(f"Connecting to live measurement data endpoint with query: {' '.join(query.split())}")
265268
document_node_query = parse(query)
266269

267-
async for data in self.websocket_client.subscribe_async(document_node_query):
270+
async for data in self._websocket_client.subscribe_async(document_node_query):
268271
self.logger.debug("Real time data received!")
269272
self.process_websocket_response(data, exit_condition=exit_condition)
270273
if not self.websocket_running: break
@@ -288,7 +291,8 @@ async def retrieve_from_websocket():
288291
await asyncio.sleep(retry_interval)
289292
finally:
290293
self.websocket_running = False
291-
await self.websocket_client.close_async()
294+
if self._websocket_client:
295+
await self._websocket_client.close_async()
292296

293297
if retry_attempts >= retries:
294298
self.logger.critical(f"Could not connect to the websocket, even after {retry_attempts} tries.")

tibber/types/live_measurement.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,17 @@ def max_power(self) -> float:
8080
@property
8181
def power_production(self) -> float:
8282
"""Net production (A-) at the moment (Watt)"""
83-
return self.cache.get("powerProduction") # pragma: no cover
83+
return self.cache.get("powerProduction")
8484

8585
@property
8686
def power_reactive(self) -> float:
8787
"""Reactive consumption (Q+) at the moment (kVAr)"""
88-
return self.cache.get("powerReactive") # pragma: no cover
88+
return self.cache.get("powerReactive")
8989

9090
@property
9191
def power_production_reactive(self) -> float:
9292
"""Net reactive production (Q-) at the moment (kVAr)"""
93-
return self.cache.get("powerProductionReactive") # pragma: no cover
93+
return self.cache.get("powerProductionReactive")
9494

9595
@property
9696
def min_power_production(self) -> float:
@@ -110,57 +110,57 @@ def last_meter_production(self) -> float:
110110
@property
111111
def power_factor(self) -> float:
112112
"""Power factor (active power / apparent power)"""
113-
return self.cache.get("powerFactor") # pragma: no cover
113+
return self.cache.get("powerFactor")
114114

115115
@property
116116
def voltage_phase_1(self) -> float:
117117
"""Voltage on phase 1; on Kaifa and Aidon meters the value is not part
118118
of every HAN data frame therefore the value is null at timestamps with
119119
second value other than 0, 10, 20, 30, 40, 50. There can be other deviations
120120
based on concrete meter firmware."""
121-
return self.cache.get("voltagePhase1") # pragma: no cover
121+
return self.cache.get("voltagePhase1")
122122

123123
@property
124124
def voltage_phase_2(self) -> float:
125125
"""Voltage on phase 2; on Kaifa and Aidon meters the value is not part
126126
of every HAN data frame therefore the value is null at timestamps with
127127
second value other than 0, 10, 20, 30, 40, 50. There can be other deviations
128128
based on concrete meter firmware."""
129-
return self.cache.get("voltagePhase2") # pragma: no cover
129+
return self.cache.get("voltagePhase2")
130130

131131
@property
132132
def voltage_phase_3(self) -> float:
133133
"""Voltage on phase 3; on Kaifa and Aidon meters the value is not part
134134
of every HAN data frame therefore the value is null at timestamps with
135135
second value other than 0, 10, 20, 30, 40, 50. There can be other deviations
136136
based on concrete meter firmware."""
137-
return self.cache.get("voltagePhase3") # pragma: no cover
137+
return self.cache.get("voltagePhase3")
138138

139139
@property
140140
def currentL1(self) -> float:
141141
"""Current on L1; on Kaifa and Aidon meters the value is not part of
142142
every HAN data frame therefore the value is null at timestamps with
143143
second value other than 0, 10, 20, 30, 40, 50. There can be other deviations
144144
based on concrete meter firmware."""
145-
return self.cache.get("currentL1") # pragma: no cover
145+
return self.cache.get("currentL1")
146146

147147
@property
148148
def currentL2(self) -> float:
149149
"""Current on L2; on Kaifa and Aidon meters the value is not part of
150150
every HAN data frame therefore the value is null at timestamps with
151151
second value other than 0, 10, 20, 30, 40, 50. There can be other deviations
152152
based on concrete meter firmware."""
153-
return self.cache.get("currentL2") # pragma: no cover
153+
return self.cache.get("currentL2")
154154

155155
@property
156156
def currentL3(self) -> float:
157157
"""Current on L3; on Kaifa and Aidon meters the value is not part of
158158
every HAN data frame therefore the value is null at timestamps with
159159
second value other than 0, 10, 20, 30, 40, 50. There can be other deviations
160160
based on concrete meter firmware."""
161-
return self.cache.get("currentL3") # pragma: no cover
161+
return self.cache.get("currentL3")
162162

163163
@property
164-
def signal_strength(self) -> int:
164+
def signal_strength(self) -> int: # pragma: no cover
165165
"""Device signal strength (Pulse - dB; Watty - percent)"""
166-
return self.cache.get("signalStrength") # pragma: no cover
166+
return self.cache.get("signalStrength")

0 commit comments

Comments
 (0)