Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions contxt/generated/nionic_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ def query_main_services():
_op_main_services_nodes_demand.name()
_op_main_services_nodes_demand.alias()
_op_main_services_nodes_demand.data_type()
_op_main_services_nodes_meter_usage = _op_main_services_nodes.meter_usage()
_op_main_services_nodes_meter_usage.id()
_op_main_services_nodes_meter_usage.data_source_name()
_op_main_services_nodes_meter_usage.name()
_op_main_services_nodes_meter_usage.alias()
_op_main_services_nodes_meter_usage.data_type()
_op_main_services_nodes_meter_demand = _op_main_services_nodes.meter_demand()
_op_main_services_nodes_meter_demand.id()
_op_main_services_nodes_meter_demand.data_source_name()
_op_main_services_nodes_meter_demand.name()
_op_main_services_nodes_meter_demand.alias()
_op_main_services_nodes_meter_demand.data_type()
_op_main_services_nodes_grid_usage = _op_main_services_nodes.grid_usage()
_op_main_services_nodes_grid_usage.id()
_op_main_services_nodes_grid_usage.data_source_name()
_op_main_services_nodes_grid_usage.name()
_op_main_services_nodes_grid_usage.alias()
_op_main_services_nodes_grid_usage.data_type()
_op_main_services_nodes_grid_demand = _op_main_services_nodes.grid_demand()
_op_main_services_nodes_grid_demand.id()
_op_main_services_nodes_grid_demand.data_source_name()
_op_main_services_nodes_grid_demand.name()
_op_main_services_nodes_grid_demand.alias()
_op_main_services_nodes_grid_demand.data_type()
_op_main_services_nodes.created_at()
_op_main_services_nodes.updated_at()
return _op
Expand Down
5 changes: 5 additions & 0 deletions contxt/models/ems.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@


class ResourceType(Enum):
AIRFLOW_ELECTRIC_HIGH = "airflow_electric_high"
AIRFLOW_ELECTRIC_LOW = "airflow_electric_low"
AIRFLOW_HIGH = "airflow_high"
AIRFLOW_LOW = "airflow_low"
COMBINED = "combined"
ELECTRIC = "electric"
GAS = "gas"
SOLAR = "solar"
WATER = "water"


Expand Down
28 changes: 28 additions & 0 deletions graphql/nionic_queries.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,34 @@ query mainServices($facilityId: Int!) {
alias
dataType
}
meterUsage {
id
dataSourceName
name
alias
dataType
}
meterDemand {
id
dataSourceName
name
alias
dataType
}
gridUsage {
id
dataSourceName
name
alias
dataType
}
gridDemand {
id
dataSourceName
name
alias
dataType
}
createdAt
updatedAt
}
Expand Down
154 changes: 154 additions & 0 deletions tests/services/test_nionic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from unittest.mock import MagicMock, patch

import pytest

from contxt.models.ems import ResourceType
from contxt.services.nionic import NionicService


def _make_datapoint(id, name, data_source_name="ds1", alias=None, data_type="FLOAT"):
return {
"id": id,
"dataSourceName": data_source_name,
"name": name,
"alias": alias or name,
"dataType": data_type,
}


def _make_main_service(
id,
facility_id,
name,
type_,
usage=None,
demand=None,
meter_usage=None,
meter_demand=None,
grid_usage=None,
grid_demand=None,
):
return {
"id": id,
"facilityId": facility_id,
"name": name,
"type": type_,
"usage": usage,
"demand": demand,
"meterUsage": meter_usage,
"meterDemand": meter_demand,
"gridUsage": grid_usage,
"gridDemand": grid_demand,
"createdAt": "2024-01-01T00:00:00",
"updatedAt": "2024-01-01T00:00:00",
}


ELECTRIC_SERVICE = _make_main_service(
id=1,
facility_id=100,
name="Main Electric",
type_="ELECTRIC",
usage=_make_datapoint(10, "electric_usage"),
demand=_make_datapoint(11, "electric_demand"),
meter_usage=_make_datapoint(12, "electric_meter_usage"),
meter_demand=_make_datapoint(13, "electric_meter_demand"),
grid_usage=_make_datapoint(14, "electric_grid_usage"),
grid_demand=_make_datapoint(15, "electric_grid_demand"),
)

SOLAR_SERVICE = _make_main_service(
id=2,
facility_id=100,
name="Main Solar",
type_="SOLAR",
usage=_make_datapoint(20, "solar_usage"),
demand=_make_datapoint(21, "solar_demand"),
meter_usage=None,
meter_demand=None,
grid_usage=None,
grid_demand=None,
)


def _mock_run_response(services):
"""Build the raw JSON dict that NionicService.run() would return from the endpoint."""
return {"data": {"mainServices": {"nodes": services}}}


@pytest.fixture
def nionic_service():
"""Create a NionicService with mocked auth, bypassing real HTTP setup."""
mock_auth = MagicMock()
mock_auth.get_token.return_value = "fake-token"
with patch.object(NionicService, "__init__", lambda self, *a, **kw: None):
svc = NionicService.__new__(NionicService)
return svc


class TestGetMainServices:
def test_returns_all_services(self, nionic_service):
response = _mock_run_response([ELECTRIC_SERVICE, SOLAR_SERVICE])
with patch.object(nionic_service, "run", return_value=response):
results = nionic_service.get_main_services(facility_id=100)

assert len(results) == 2
assert results[0].name == "Main Electric"
assert results[1].name == "Main Solar"

def test_all_datapoint_fields_populated(self, nionic_service):
response = _mock_run_response([ELECTRIC_SERVICE])
with patch.object(nionic_service, "run", return_value=response):
results = nionic_service.get_main_services(facility_id=100)

svc = results[0]
assert svc.usage.id == 10
assert svc.demand.id == 11
assert svc.meter_usage.id == 12
assert svc.meter_demand.id == 13
assert svc.grid_usage.id == 14
assert svc.grid_demand.id == 15

def test_datapoint_subfields(self, nionic_service):
response = _mock_run_response([ELECTRIC_SERVICE])
with patch.object(nionic_service, "run", return_value=response):
results = nionic_service.get_main_services(facility_id=100)

dp = results[0].meter_usage
assert dp.data_source_name == "ds1"
assert dp.name == "electric_meter_usage"
assert dp.alias == "electric_meter_usage"
assert dp.data_type == "FLOAT"

def test_filters_by_resource_type(self, nionic_service):
response = _mock_run_response([ELECTRIC_SERVICE, SOLAR_SERVICE])
with patch.object(nionic_service, "run", return_value=response):
results = nionic_service.get_main_services(
facility_id=100, resource_type=ResourceType.ELECTRIC
)

assert len(results) == 1
assert results[0].name == "Main Electric"

def test_filters_by_solar_type(self, nionic_service):
response = _mock_run_response([ELECTRIC_SERVICE, SOLAR_SERVICE])
with patch.object(nionic_service, "run", return_value=response):
results = nionic_service.get_main_services(facility_id=100, resource_type=ResourceType.SOLAR)

assert len(results) == 1
assert results[0].name == "Main Solar"

def test_null_datapoints(self, nionic_service):
"""When meter/grid fields are null, sgqlc returns empty falsy DataPoint objects."""
response = _mock_run_response([SOLAR_SERVICE])
with patch.object(nionic_service, "run", return_value=response):
results = nionic_service.get_main_services(facility_id=100)

svc = results[0]
assert svc.usage.id == 20
assert svc.demand.id == 21
# sgqlc deserializes null relationships as empty DataPoint objects (falsy, no attributes)
assert not svc.meter_usage
assert not svc.meter_demand
assert not svc.grid_usage
assert not svc.grid_demand
Loading