Skip to content

Commit

Permalink
Systems and services improvements (#4879)
Browse files Browse the repository at this point in the history
* systen service modal mods

* update meter enums

* property display field

* alphabetical actions

* nav error and modal arrangement

* precommit

* unit refactors

* precommit

* fix tests

---------

Co-authored-by: Ross Perry <[email protected]>
  • Loading branch information
perryr16 and Ross Perry authored Nov 27, 2024
1 parent e5df2f4 commit 53bb3e9
Show file tree
Hide file tree
Showing 29 changed files with 433 additions and 232 deletions.
91 changes: 91 additions & 0 deletions seed/migrations/0237_auto_20241106_1339.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Generated by Django 3.2.25 on 2024-11-06 21:39

import quantityfield.fields
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("seed", "0236_auto_20241030_1434"),
]

operations = [
migrations.AlterField(
model_name="batterysystem",
name="efficiency",
field=models.FloatField(),
),
migrations.AlterField(
model_name="meter",
name="connection_type",
field=models.IntegerField(
choices=[
(1, "Imported"),
(2, "Exported"),
(3, "Receiving Service"),
(4, "Returning To Service"),
(5, "Total From Users"),
(6, "Total To Users"),
],
default=1,
),
),
migrations.AlterField(
model_name="service",
name="emission_factor",
field=models.FloatField(null=True),
),
migrations.RemoveField(
model_name="batterysystem",
name="capacity",
),
migrations.RemoveField(
model_name="dessystem",
name="capacity",
),
migrations.AddField(
model_name="batterysystem",
name="energy_capacity",
field=quantityfield.fields.QuantityField(base_units="kWh", default=1, unit_choices=["kWh"]),
preserve_default=False,
),
migrations.AddField(
model_name="batterysystem",
name="power_capacity",
field=quantityfield.fields.QuantityField(base_units="kW", default=1, unit_choices=["kW"]),
preserve_default=False,
),
migrations.AddField(
model_name="dessystem",
name="cooling_capacity",
field=quantityfield.fields.QuantityField(base_units="Ton", null=True, unit_choices=["Ton"]),
),
migrations.AddField(
model_name="dessystem",
name="heating_capacity",
field=quantityfield.fields.QuantityField(base_units="MMBtu", null=True, unit_choices=["MMBtu"]),
),
migrations.AddField(
model_name="evsesystem",
name="voltage",
field=quantityfield.fields.QuantityField(base_units="V", default=1, unit_choices=["V"]),
preserve_default=False,
),
migrations.AlterField(
model_name="batterysystem",
name="voltage",
field=quantityfield.fields.QuantityField(base_units="V", unit_choices=["V"]),
),
migrations.AlterField(
model_name="evsesystem",
name="power",
field=quantityfield.fields.QuantityField(base_units="kW", unit_choices=["kW"]),
),
migrations.AddConstraint(
model_name="dessystem",
constraint=models.CheckConstraint(
check=models.Q(("heating_capacity__isnull", False), ("cooling_capacity__isnull", False), _connector="OR"),
name="heating_or_cooling_capacity_required",
),
),
]
32 changes: 16 additions & 16 deletions seed/models/meters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@
class Meter(models.Model):
## CONNECTION TYPES
# These connection types do not require services. May be on a property or system
FROM_OUTSIDE = 1 # tracks what is received via an unknown source
TO_OUTSIDE = 2 # tracks what is expelled via an unknown source
IMPORTED = 1 # tracks what is received via an unknown source
EXPORTED = 2 # tracks what is expelled via an unknown source

# These connection types require services. May be on a property or system
FROM_SERVICE_TO_PATRON = 3 # tracks what is received via my service
FROM_PATRON_TO_SERVICE = 4 # tracks what is expelled via my service
RECEIVING_SERVICE = 3 # tracks what is received via my service
RETURNING_TO_SERVICE = 4 # tracks what is expelled via my service

# These connection types require services and may only be on Systems
TOTAL_FROM_PATRON = 5 # tracks everything that this system expelled via my service
TOTAL_TO_PATRON = 6 # tracks everything that this system received via my service
TOTAL_FROM_USERS = 5 # tracks everything that this system expelled via my service
TOTAL_TO_USERS = 6 # tracks everything that this system received via my service

CONNECTION_TYPES = (
(FROM_OUTSIDE, "From Outside"),
(TO_OUTSIDE, "To Outside"),
(FROM_SERVICE_TO_PATRON, "From Service To Patron"),
(FROM_PATRON_TO_SERVICE, "From Patron To Service"),
(TOTAL_FROM_PATRON, "Total From Patron"),
(TOTAL_TO_PATRON, "Total To Patron"),
(IMPORTED, "Imported"),
(EXPORTED, "Exported"),
(RECEIVING_SERVICE, "Receiving Service"),
(RETURNING_TO_SERVICE, "Returning To Service"),
(TOTAL_FROM_USERS, "Total From Users"),
(TOTAL_TO_USERS, "Total To Users"),
)

COAL_ANTHRACITE = 1
Expand Down Expand Up @@ -165,7 +165,7 @@ class Meter(models.Model):
type = models.IntegerField(choices=ENERGY_TYPES, default=None, null=True)

service = models.ForeignKey("Service", on_delete=models.SET_NULL, related_name="meters", null=True, blank=True)
connection_type = models.IntegerField(choices=CONNECTION_TYPES, default=FROM_OUTSIDE, null=False)
connection_type = models.IntegerField(choices=CONNECTION_TYPES, default=IMPORTED, null=False)

def copy_readings(self, source_meter, overlaps_possible=True):
"""
Expand Down Expand Up @@ -218,7 +218,7 @@ def presave_meter(sender, instance, **kwargs):
if property is not None and system is not None:
raise IntegrityError(f"Meter {instance.id} has both a property and a system. It must only have one.")

outside_connection = connection_type in [Meter.FROM_OUTSIDE, Meter.TO_OUTSIDE]
outside_connection = connection_type in [Meter.IMPORTED, Meter.EXPORTED]
if outside_connection:
# outside connections don't have services
if instance.service is not None:
Expand All @@ -228,7 +228,7 @@ def presave_meter(sender, instance, **kwargs):
if service is None:
raise IntegrityError(f"Meter {instance.id} has connection_type '{connection_string}', but is not connected to a service")

total_connections = connection_type in [Meter.TOTAL_FROM_PATRON, Meter.TOTAL_TO_PATRON]
total_connections = connection_type in [Meter.TOTAL_FROM_USERS, Meter.TOTAL_TO_USERS]
if total_connections:
# Only systems have connection type "total"
if system is None:
Expand All @@ -245,7 +245,7 @@ def presave_meter(sender, instance, **kwargs):
if Meter.objects.filter(service=service, connection_type=connection_type).exclude(pk=instance.pk).exists():
raise IntegrityError(f"Service {service.id} already has a meter with connection type '{connection_string}'")

elif property: # Meter.FROM_PATRON_TO_SERVICE and Meter.FROM_SERVICE_TO_PATRON
elif property: # Meter.RETURNING_TO_SERVICE and Meter.RECEIVING_SERVICE
# service must be within the meter's property's group
property_groups = InventoryGroupMapping.objects.filter(property=property).values_list("group_id", flat=True)
if service is not None and service.system.group.id not in property_groups:
Expand Down
28 changes: 22 additions & 6 deletions seed/models/systems.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
"""

from django.db import models
from django.db.models import Q
from model_utils.managers import InheritanceManager
from quantityfield.fields import QuantityField
from quantityfield.units import ureg

from seed.models import InventoryGroup

ureg.define("MMBtu = 1e6 * Btu")
ureg.define("Ton = 12000 * Btu / hour")


class System(models.Model):
name = models.CharField(max_length=255)
Expand All @@ -32,9 +38,17 @@ class DESSystem(System):
(CHP, "CHP"),
)
type = models.IntegerField(choices=DES_TYPES, null=False)
capacity = models.IntegerField(null=False)
heating_capacity = QuantityField("MMBtu", null=True)
cooling_capacity = QuantityField("Ton", null=True)
count = models.IntegerField(default=1, null=False)

class Meta:
constraints = [
models.CheckConstraint(
check=Q(heating_capacity__isnull=False) | Q(cooling_capacity__isnull=False), name="heating_or_cooling_capacity_required"
)
]


class EVSESystem(System):
LEVEL1 = 0
Expand All @@ -47,20 +61,22 @@ class EVSESystem(System):
(LEVEL3, "Level3-DC Fast"),
)
type = models.IntegerField(choices=EVSE_TYPES, null=False)
power = models.IntegerField(null=False)
power = QuantityField("kW", null=False)
voltage = QuantityField("V", null=False)
count = models.IntegerField(default=1, null=False)


class BatterySystem(System):
efficiency = models.IntegerField(null=False)
capacity = models.IntegerField(null=False)
voltage = models.IntegerField(null=False)
efficiency = models.FloatField(null=False)
power_capacity = QuantityField("kW", null=False)
energy_capacity = QuantityField("kWh", null=False)
voltage = QuantityField("V", null=False)


class Service(models.Model):
system = models.ForeignKey(System, on_delete=models.CASCADE, related_name="services")
name = models.CharField(max_length=255)
emission_factor = models.IntegerField(null=True)
emission_factor = models.FloatField(null=True)

objects = InheritanceManager()

Expand Down
23 changes: 13 additions & 10 deletions seed/serializers/meters.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,25 @@ def validate_scenario_id(self, scenario_id):

def to_representation(self, obj):
result = super().to_representation(obj)

if obj.source == Meter.GREENBUTTON:
result["source_id"] = usage_point_id(obj.source_id)

result["scenario_name"] = obj.scenario.name if obj.scenario else None

if obj.alias is None or obj.alias == "":
result["alias"] = f"{obj.get_type_display()} - {obj.get_source_display()} - {result['source_id']}"

self.get_property_display_name(obj, result)
self.set_config(obj, result)

return result

def set_config(self, obj, result):
# generate config for meter modal
connection_lookup = {
1: {"direction": "inflow", "use": "outside", "connection": "outside"},
2: {"direction": "outflow", "use": "outside", "connection": "outside"},
3: {"direction": "inflow", "use": "using", "connection": "service"},
4: {"direction": "outflow", "use": "using", "connection": "service"},
5: {"direction": "inflow", "use": "offering", "connection": "service"},
6: {"direction": "outflow", "use": "offering", "connection": "service"},
1: {"direction": "imported", "use": "outside", "connection": "outside"},
2: {"direction": "exported", "use": "outside", "connection": "outside"},
3: {"direction": "imported", "use": "using", "connection": "service"},
4: {"direction": "exported", "use": "using", "connection": "service"},
5: {"direction": "imported", "use": "offering", "connection": "service"},
6: {"direction": "exported", "use": "offering", "connection": "service"},
}

group_id, system_id = None, None
Expand All @@ -80,3 +77,9 @@ def set_config(self, obj, result):

config = {"group_id": group_id, "system_id": system_id, "service_id": obj.service_id, **connection_lookup[obj.connection_type]}
result["config"] = config

def get_property_display_name(self, obj, result):
if obj.property:
state = obj.property.views.first().state
property_display_field = state.organization.property_display_field
result["property_display_field"] = getattr(state, property_display_field, "Unknown")
3 changes: 2 additions & 1 deletion seed/serializers/pint.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ def collapse_unit(org, x):

if isinstance(x, ureg.Quantity):
dimensionality = get_dimensionality(x)
pint_spec = pint_specs[dimensionality]
# default to quantity's units if not found
pint_spec = pint_specs.get(dimensionality, x.units)
converted_value = x.to(pint_spec).magnitude
return round(converted_value, org.display_decimal_places)
elif isinstance(x, list):
Expand Down
36 changes: 18 additions & 18 deletions seed/serializers/systems.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from seed.data_importer.utils import usage_point_id
from seed.models import BatterySystem, DESSystem, EVSESystem, Service, System
from seed.serializers.base import ChoiceField
from seed.serializers.pint import collapse_unit


class ServiceSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -70,63 +71,62 @@ class Meta:
fields = ["id", "name", "services", "type", "group_id"]

def validate(self, data):
SystemClass = self.Meta.model
id = self.instance.id if self.instance else None

if SystemClass.objects.filter(name=data.get("name"), group=data.get("group_id")).exclude(id=id).exists():
if System.objects.filter(name=data.get("name"), group=data.get("group_id")).exclude(id=id).exists():
raise serializers.ValidationError("System name must be unique within group")
return data


class DESSystemSerializer(SystemSerializer):
des_type = ChoiceField(source="type", choices=DESSystem.DES_TYPES)
capacity = serializers.IntegerField()
count = serializers.IntegerField()

class Meta:
model = DESSystem
fields = [*SystemSerializer.Meta.fields, "des_type", "capacity", "count"]
fields = [*SystemSerializer.Meta.fields, "cooling_capacity", "count", "des_type", "heating_capacity"]

def to_representation(self, obj):
org = obj.group.organization
mode = "Cooling" if obj.cooling_capacity else "Heating"
return {
"type": "DES",
"des_type": obj.get_type_display(),
"capacity": obj.capacity,
"cooling_capacity": collapse_unit(org, obj.cooling_capacity),
"count": obj.count,
"heating_capacity": collapse_unit(org, obj.heating_capacity),
"mode": mode,
}


class EVSESystemSerializer(SystemSerializer):
evse_type = ChoiceField(source="type", choices=EVSESystem.EVSE_TYPES)
power = serializers.IntegerField()
count = serializers.IntegerField()

class Meta:
model = EVSESystem
fields = [*SystemSerializer.Meta.fields, "evse_type", "power", "count"]
fields = [*SystemSerializer.Meta.fields, "count", "evse_type", "power", "voltage"]

def to_representation(self, obj):
org = obj.group.organization
return {
"type": "EVSE",
"evse_type": obj.get_type_display(),
"power": obj.power,
"count": obj.count,
"power": collapse_unit(org, obj.power),
"voltage": collapse_unit(org, obj.voltage),
"count": collapse_unit(org, obj.count),
}


class BatterySystemSerializer(SystemSerializer):
efficiency = serializers.IntegerField()
capacity = serializers.IntegerField()
voltage = serializers.IntegerField()

class Meta:
model = BatterySystem
fields = [*SystemSerializer.Meta.fields, "efficiency", "capacity", "voltage"]
fields = [*SystemSerializer.Meta.fields, "energy_capacity", "power_capacity", "efficiency", "voltage"]

def to_representation(self, obj):
org = obj.group.organization
return {
"type": "Battery",
"efficiency": obj.efficiency,
"capacity": obj.capacity,
"voltage": obj.voltage,
"energy_capacity": collapse_unit(org, obj.energy_capacity),
"power_capacity": collapse_unit(org, obj.power_capacity),
"voltage": collapse_unit(org, obj.voltage),
}
Loading

0 comments on commit 53bb3e9

Please sign in to comment.