Compare commits
No commits in common. "feature/f31-device-enviromental-impact" and "main" have entirely different histories.
feature/f3
...
main
|
@ -29,17 +29,14 @@
|
|||
<li class="nav-item">
|
||||
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
|
||||
</li>
|
||||
{% if dpps %}
|
||||
<li class="nav-item">
|
||||
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if dpps %}
|
||||
<li class="nav-item">
|
||||
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#environmental_impact" class="nav-link" data-bs-toggle="tab" data-bs-target="#environmental_impact">{% trans 'Environmental impact' %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -245,104 +242,23 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="environmental_impact">
|
||||
<div class="container-fluid py-3">
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a class="btn btn-success">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
{% trans 'Export to PDF' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-arrow-down-circle text-success" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-success">Carbon Reduction</h5>
|
||||
<h2 class="mb-2">{{ impact.carbon_saved }}</h2>
|
||||
<p class="card-text text-muted">kg CO₂e saved</p>
|
||||
</div>
|
||||
{% if dpps %}
|
||||
<div class="tab-pane fade" id="dpps">
|
||||
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
|
||||
<div class="list-group col">
|
||||
{% for d in dpps %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<small class="text-muted">{{ d.timestamp }}</small>
|
||||
<span>{{ d.type }}</span>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
<a href="{% url 'did:device_web' d.signature %}">{{ d.signature }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-danger">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-cloud-fill text-danger" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-danger">Carbon Consumed</h5>
|
||||
<h2 class="mb-2">{{ impact.co2_emissions }}</h2>
|
||||
<p class="card-text text-muted">kg CO₂e consumed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-recycle text-success" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-success">Additional Impact Metric</h5>
|
||||
<h2 class="mb-2">85%</h2>
|
||||
<p class="card-text text-muted">whatever</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Impact Details</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="bg-light" style="width: 30%;">Manufacturing Impact Avoided</th>
|
||||
<td>
|
||||
<span class="text-success">{{ impact.carbon_saved }}</span> kg CO₂e
|
||||
<br />
|
||||
<small class="text-muted">Based on average laptop manufacturing emissions</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>Calculation Method</h6>
|
||||
<small class="text-muted">Based on industry standards X Y and Z</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if dpps %}
|
||||
<div class="tab-pane fade" id="dpps">
|
||||
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
|
||||
<div class="list-group col">
|
||||
{% for d in dpps %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<small class="text-muted">{{ d.timestamp }}</small>
|
||||
<span>{{ d.type }}</span>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
<a href="{% url 'did:device_web' d.signature %}">{{ d.signature }}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,5 +9,6 @@ urlpatterns = [
|
|||
path("<str:pk>/", views.DetailsView.as_view(), name="details"),
|
||||
path("<str:pk>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"),
|
||||
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
|
||||
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web")
|
||||
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
|
||||
|
||||
]
|
||||
|
|
|
@ -14,7 +14,6 @@ from evidence.models import Annotation
|
|||
from lot.models import LotTag
|
||||
from device.models import Device
|
||||
from device.forms import DeviceFormSet
|
||||
from environmental_impact.algorithms.algorithm_factory import FactoryEnvironmentImpactAlgorithm
|
||||
if settings.DPP:
|
||||
from dpp.models import Proof
|
||||
from dpp.api_dlt import PROOF_TYPE
|
||||
|
@ -111,16 +110,10 @@ class DetailsView(DashboardView, TemplateView):
|
|||
uuid__in=self.object.uuids,
|
||||
type=PROOF_TYPE["IssueDPP"]
|
||||
)
|
||||
enviromental_impact_algorithm = FactoryEnvironmentImpactAlgorithm.run_environmental_impact_calculation(
|
||||
"dummy_calc"
|
||||
)
|
||||
enviromental_impact = enviromental_impact_algorithm.get_device_environmental_impact(
|
||||
self.object)
|
||||
context.update({
|
||||
'object': self.object,
|
||||
'snapshot': self.object.get_last_evidence(),
|
||||
'lot_tags': lot_tags,
|
||||
'impact': enviromental_impact,
|
||||
'dpps': dpps,
|
||||
})
|
||||
return context
|
||||
|
|
|
@ -89,7 +89,6 @@ INSTALLED_APPS = [
|
|||
"dashboard",
|
||||
"admin",
|
||||
"api",
|
||||
"environmental_impact"
|
||||
]
|
||||
|
||||
DPP = config("DPP", default=False, cast=bool)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,30 +0,0 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .dummy_calculator import DummyEnvironmentalImpactAlgorithm
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .algorithm_interface import EnvironmentImpactAlgorithm
|
||||
|
||||
|
||||
class AlgorithmNames():
|
||||
"""
|
||||
Enum class for the different types of algorithms.
|
||||
"""
|
||||
|
||||
DUMMY_CALC = "dummy_calc"
|
||||
|
||||
algorithm_names = {
|
||||
DUMMY_CALC: DummyEnvironmentalImpactAlgorithm()
|
||||
}
|
||||
|
||||
|
||||
class FactoryEnvironmentImpactAlgorithm():
|
||||
|
||||
@staticmethod
|
||||
def run_environmental_impact_calculation(algorithm_name: str) -> EnvironmentImpactAlgorithm:
|
||||
try:
|
||||
return AlgorithmNames.algorithm_names[algorithm_name]
|
||||
except KeyError:
|
||||
raise ValueError("Invalid algorithm name. Valid options are: " +
|
||||
", ".join(AlgorithmNames.algorithm_names.keys()))
|
|
@ -1,11 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from functools import lru_cache
|
||||
from device.models import Device
|
||||
from environmental_impact.models import EnvironmentalImpact
|
||||
|
||||
|
||||
class EnvironmentImpactAlgorithm(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def get_device_environmental_impact(self, device: Device) -> EnvironmentalImpact:
|
||||
pass
|
|
@ -1,33 +0,0 @@
|
|||
from device.models import Device
|
||||
from .algorithm_interface import EnvironmentImpactAlgorithm
|
||||
from environmental_impact.models import EnvironmentalImpact
|
||||
|
||||
|
||||
class DummyEnvironmentalImpactAlgorithm(EnvironmentImpactAlgorithm):
|
||||
|
||||
def get_device_environmental_impact(self, device: Device) -> EnvironmentalImpact:
|
||||
# TODO Make a constants file / class
|
||||
avg_watts = 40 # Arbitrary laptop average consumption
|
||||
co2_per_kwh = 0.475
|
||||
power_on_hours = self.get_power_on_hours_from(device)
|
||||
energy_kwh = (power_on_hours * avg_watts) / 1000
|
||||
co2_emissions = energy_kwh * co2_per_kwh
|
||||
return EnvironmentalImpact(co2_emissions=co2_emissions)
|
||||
|
||||
def get_power_on_hours_from(self, device: Device) -> int:
|
||||
# TODO how do I check if the device is a legacy workbench? Is there a better way?
|
||||
is_legacy_workbench = False if device.last_evidence.inxi else True
|
||||
if not is_legacy_workbench:
|
||||
storage_components = device.components[9]
|
||||
str_time = storage_components.get('time of used', -1)
|
||||
else:
|
||||
str_time = ""
|
||||
uptime_in_hours = self.convert_str_time_to_hours(str_time, is_legacy_workbench)
|
||||
return uptime_in_hours
|
||||
|
||||
def convert_str_time_to_hours(self, time_str: str, is_legacy_workbench: bool) -> int:
|
||||
if is_legacy_workbench:
|
||||
return -1 # TODO Power on hours not available in legacy workbench
|
||||
else:
|
||||
multipliers = {'y': 365 * 24, 'd': 24, 'h': 1}
|
||||
return sum(int(part[:-1]) * multipliers[part[-1]] for part in time_str.split())
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EnvironmentalImpactConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "environmental_impact"
|
|
@ -1,8 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
from django.db import models
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvironmentalImpact:
|
||||
carbon_saved: float = 0.0
|
||||
co2_emissions: float = 0.0
|
|
@ -1,44 +0,0 @@
|
|||
from unittest.mock import patch
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from device.models import Device
|
||||
from environmental_impact.models import EnvironmentalImpact
|
||||
from environmental_impact.algorithms.dummy_calculator import DummyEnvironmentalImpactAlgorithm
|
||||
from evidence.models import Evidence
|
||||
|
||||
|
||||
class DummyEnvironmentalImpactAlgorithmTests(TestCase):
|
||||
|
||||
@patch('evidence.models.Evidence.get_doc', return_value={'credentialSubject': {}})
|
||||
@patch('evidence.models.Evidence.get_time', return_value=None)
|
||||
def setUp(self, mock_get_time, mock_get_doc):
|
||||
self.device = Device(id='1')
|
||||
evidence = self.device.last_evidence = Evidence(uuid=uuid.uuid4())
|
||||
evidence.inxi = True
|
||||
evidence.doc = {'credentialSubject': {}}
|
||||
self.algorithm = DummyEnvironmentalImpactAlgorithm()
|
||||
|
||||
def test_get_power_on_hours_from_legacy_device(self):
|
||||
# TODO is there a way to check that?
|
||||
pass
|
||||
|
||||
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
|
||||
def test_get_power_on_hours_from_inxi_device(self, mock_get_components):
|
||||
hours = self.algorithm.get_power_on_hours_from(self.device)
|
||||
self.assertEqual(
|
||||
hours, 8811, "Inxi-parsed devices should correctly compute power-on hours")
|
||||
|
||||
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
|
||||
def test_convert_str_time_to_hours(self, mock_get_components):
|
||||
result = self.algorithm.convert_str_time_to_hours('1y 2d 3h', False)
|
||||
self.assertEqual(
|
||||
result, 8811, "String to hours conversion should match expected output")
|
||||
|
||||
@patch('evidence.models.Evidence.get_components', return_value=[0, 0, 0, 0, 0, 0, 0, 0, 0, {'time of used': '1y 2d 3h'}])
|
||||
def test_environmental_impact_calculation(self, mock_get_components):
|
||||
impact = self.algorithm.get_device_environmental_impact(self.device)
|
||||
self.assertIsInstance(impact, EnvironmentalImpact,
|
||||
"Output should be an EnvironmentalImpact instance")
|
||||
expected_co2 = 8811 * 40 * 0.475 / 1000
|
||||
self.assertAlmostEqual(impact.co2_emissions, expected_co2,
|
||||
2, "CO2 emissions calculation should be accurate")
|
|
@ -1,17 +0,0 @@
|
|||
from environmental_impact.algorithms.algorithm_factory import FactoryEnvironmentImpactAlgorithm
|
||||
from django.test import TestCase
|
||||
from environmental_impact.algorithms.dummy_calculator import DummyEnvironmentalImpactAlgorithm
|
||||
|
||||
|
||||
class FactoryEnvironmentImpactAlgorithmTests(TestCase):
|
||||
|
||||
def test_valid_algorithm_name(self):
|
||||
algorithm = FactoryEnvironmentImpactAlgorithm.run_environmental_impact_calculation(
|
||||
'dummy_calc')
|
||||
self.assertIsInstance(algorithm, DummyEnvironmentalImpactAlgorithm,
|
||||
"Factory should return a DummyEnvironmentalImpactAlgorithm instance")
|
||||
|
||||
def test_invalid_algorithm_name(self):
|
||||
with self.assertRaises(ValueError):
|
||||
FactoryEnvironmentImpactAlgorithm.run_environmental_impact_calculation(
|
||||
'invalid_calc')
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
Loading…
Reference in a new issue