Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Systems and services improvements #4879

Merged
merged 9 commits into from
Nov 27, 2024
Merged
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
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
Loading