Add generic ENLYZE power meter pipeline
This commit is contained in:
@@ -9,6 +9,7 @@ from production_analytics.enlyze.exploration import (
|
||||
AuthenticationError,
|
||||
ExplorationClient,
|
||||
ExplorationSettings,
|
||||
HttpResponseError,
|
||||
NonJsonResponseError,
|
||||
load_secret_file,
|
||||
)
|
||||
@@ -78,6 +79,28 @@ class ExplorationClientTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(AuthenticationError, "HTTP 401"):
|
||||
ExplorationClient(self.settings).get("/observed/path")
|
||||
|
||||
@patch("production_analytics.enlyze.exploration.urlopen")
|
||||
def test_http_error_keeps_sanitized_bounded_body(self, urlopen_mock: object) -> None:
|
||||
urlopen_mock.side_effect = HTTPError(
|
||||
"https://example.invalid", 422, "Unprocessable", {},
|
||||
io.BytesIO(b'{"message":"bad variable","token":"must not echo"}'),
|
||||
) # type: ignore[attr-defined]
|
||||
|
||||
with self.assertRaises(HttpResponseError) as caught:
|
||||
ExplorationClient(self.settings).get("/observed/path")
|
||||
self.assertEqual(caught.exception.response_body["message"], "bad variable")
|
||||
self.assertEqual(caught.exception.response_body["token"], "<redacted-secret>")
|
||||
|
||||
@patch("production_analytics.enlyze.exploration.urlopen")
|
||||
def test_get_all_pages_follows_cursor(self, urlopen_mock: object) -> None:
|
||||
urlopen_mock.side_effect = [
|
||||
_Response(b'{"data":[1],"metadata":{"next_cursor":"next"}}'),
|
||||
_Response(b'{"data":[2],"metadata":{"next_cursor":null}}'),
|
||||
]
|
||||
|
||||
pages = ExplorationClient(self.settings).get_all_pages("/v2/variables", {"machine": "m"})
|
||||
self.assertEqual([page.body["data"] for page in pages], [[1], [2]])
|
||||
|
||||
|
||||
class SanitizationTests(unittest.TestCase):
|
||||
def test_sanitization_preserves_shape_and_redacts_sensitive_values(self) -> None:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from production_analytics.power_meter.config import load_power_meter_config
|
||||
from production_analytics.power_meter.gateway import PowerMeterGateway
|
||||
from production_analytics.power_meter.repository import counter_delta
|
||||
|
||||
|
||||
def test_b2_config_has_only_verified_signals() -> None:
|
||||
config = load_power_meter_config("config/power-meters.example.yaml", "B2")
|
||||
assert config.machine_uuid == "0d7955e9-5cde-4e14-9af7-5b041dc796f0"
|
||||
assert config.signals.energy_total == "0f5c0853-1aa7-4fa3-b267-ea2669024e46"
|
||||
assert config.signals.power_outages is None
|
||||
|
||||
|
||||
def test_counter_reset_starts_new_epoch() -> None:
|
||||
assert counter_delta(None, 7) == 0
|
||||
assert counter_delta(100, 105) == 5
|
||||
assert counter_delta(100, 4) == 4
|
||||
|
||||
|
||||
def test_gateway_keeps_optional_phases_and_handles_pagination() -> None:
|
||||
config = load_meter()
|
||||
client = Mock()
|
||||
client.post_json.side_effect = [
|
||||
Mock(body={"data": {"columns": ["time", "e", "p", "f", "l1"],
|
||||
"records": [["2026-09-01T00:00:00Z", 10, 4, 50, 2]]},
|
||||
"metadata": {"next_cursor": "next"}}),
|
||||
Mock(body={"data": {"columns": ["time", "e", "p", "f", "l1"],
|
||||
"records": [["2026-09-01T00:01:00Z", 11, 5, 49.9, 3]]},
|
||||
"metadata": {"next_cursor": None}}),
|
||||
]
|
||||
readings = PowerMeterGateway(client).read(config, datetime(2026, 9, 1, tzinfo=UTC),
|
||||
datetime(2026, 9, 2, tzinfo=UTC))
|
||||
assert [item.energy_total_kwh for item in readings] == [10, 11]
|
||||
assert readings[0].phase_active_power_kw == (("L1", 2.0),)
|
||||
assert client.post_json.call_args_list[1].args[1]["cursor"] == "next"
|
||||
|
||||
|
||||
def load_meter():
|
||||
from production_analytics.power_meter.config import PowerMeterConfig, PowerMeterSignals
|
||||
return PowerMeterConfig("B2", "machine", PowerMeterSignals("e", "p", "f", phase_active_power=(("L1", "l1"),)))
|
||||
Reference in New Issue
Block a user