Compare commits
84 commits
main
...
feature/f3
Author | SHA1 | Date | |
---|---|---|---|
Sergio Giménez Antón | bd4f6b7d56 | ||
Sergio Giménez Antón | f9c9c9dd7c | ||
Sergio Giménez Antón | 60ccbec369 | ||
Sergio Giménez Antón | 3fb0961815 | ||
Sergio Giménez Antón | 447946a576 | ||
Cayo Puigdefabregas | 5d190d07a3 | ||
Cayo Puigdefabregas | d1abb206e8 | ||
85bae67189 | |||
d429485651 | |||
07c25f4a92 | |||
Cayo Puigdefabregas | 14277c17cb | ||
Cayo Puigdefabregas | f7051c3130 | ||
Cayo Puigdefabregas | 09be1a2f74 | ||
Cayo Puigdefabregas | a3dd5d9639 | ||
Cayo Puigdefabregas | 3f5460b81f | ||
Cayo Puigdefabregas | bf7975bc24 | ||
8e128557c0 | |||
Cayo Puigdefabregas | 25e7e85548 | ||
Cayo Puigdefabregas | ba126491be | ||
Cayo Puigdefabregas | 81e7ba267d | ||
Cayo Puigdefabregas | 1e08f0fc0c | ||
Cayo Puigdefabregas | ebabb6b228 | ||
Cayo Puigdefabregas | 4954199610 | ||
Cayo Puigdefabregas | e84b72c70b | ||
Cayo Puigdefabregas | 99435fff85 | ||
Cayo Puigdefabregas | 6c0e77891f | ||
Cayo Puigdefabregas | a2d859494b | ||
Cayo Puigdefabregas | ea6d990e56 | ||
Cayo Puigdefabregas | 612737d46c | ||
Cayo Puigdefabregas | 30be57ee25 | ||
Cayo Puigdefabregas | 88bdabb64f | ||
Cayo Puigdefabregas | 96268c8caf | ||
7ed05f0932 | |||
Cayo Puigdefabregas | b652d7d452 | ||
Cayo Puigdefabregas | 04ecb4f2f1 | ||
Cayo Puigdefabregas | 1613eaaa44 | ||
Cayo Puigdefabregas | 06264558df | ||
Cayo Puigdefabregas | 80b4c3b4ca | ||
Cayo Puigdefabregas | e2078c7bde | ||
cfae9d4ec9 | |||
Cayo Puigdefabregas | 578fa73fe5 | ||
f1d57ff618 | |||
Cayo Puigdefabregas | 3cf8ceb5d3 | ||
Cayo Puigdefabregas | b56dc0dfda | ||
Cayo Puigdefabregas | 1c58bff515 | ||
Cayo Puigdefabregas | e6c1ede93c | ||
371845971c | |||
b4efcfb171 | |||
ac0d36ea6f | |||
6a3a2b3a2b | |||
850678fbe4 | |||
f43aaf6ac6 | |||
355ed08561 | |||
d0e46aa0b0 | |||
771b216a31 | |||
263eacda99 | |||
8fcd20f609 | |||
15fb5d3739 | |||
d7ff3c2798 | |||
0e0ad400c2 | |||
367d3a7f87 | |||
c90ed58ea0 | |||
45629db102 | |||
1e29f9562d | |||
d0cac9d1d9 | |||
8b4d1f51f6 | |||
Cayo Puigdefabregas | 34ea4bedfc | ||
Cayo Puigdefabregas | fe429e7db6 | ||
Cayo Puigdefabregas | caf2606fd9 | ||
Cayo Puigdefabregas | 73d478f517 | ||
Cayo Puigdefabregas | 0f03171076 | ||
bfdcb33538 | |||
Cayo Puigdefabregas | 271ac83d71 | ||
Cayo Puigdefabregas | f7b2687ca2 | ||
Cayo Puigdefabregas | 1dad22c3d3 | ||
Cayo Puigdefabregas | 7de6d69a6c | ||
Sergio Giménez Antón | fa5b9eec67 | ||
Cayo Puigdefabregas | 7fd42db3e4 | ||
Cayo Puigdefabregas | bed40d3ee0 | ||
Cayo Puigdefabregas | 9553ed6a4c | ||
Sergio Giménez Antón | f3c9297ffd | ||
cb6c7f6fda | |||
Cayo Puigdefabregas | a0276f439e | ||
a4d361ff9b |
|
@ -37,6 +37,9 @@
|
|||
<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>
|
||||
|
@ -242,6 +245,87 @@
|
|||
</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>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if dpps %}
|
||||
<div class="tab-pane fade" id="dpps">
|
||||
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
|
||||
|
|
|
@ -9,6 +9,5 @@ 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,6 +14,7 @@ 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
|
||||
|
@ -110,10 +111,16 @@ 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,6 +89,7 @@ INSTALLED_APPS = [
|
|||
"dashboard",
|
||||
"admin",
|
||||
"api",
|
||||
"environmental_impact"
|
||||
]
|
||||
|
||||
DPP = config("DPP", default=False, cast=bool)
|
||||
|
|
0
environmental_impact/__init__.py
Normal file
0
environmental_impact/__init__.py
Normal file
3
environmental_impact/admin.py
Normal file
3
environmental_impact/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
0
environmental_impact/algorithms/__init__.py
Normal file
0
environmental_impact/algorithms/__init__.py
Normal file
30
environmental_impact/algorithms/algorithm_factory.py
Normal file
30
environmental_impact/algorithms/algorithm_factory.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
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()))
|
11
environmental_impact/algorithms/algorithm_interface.py
Normal file
11
environmental_impact/algorithms/algorithm_interface.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
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
|
33
environmental_impact/algorithms/dummy_calculator.py
Normal file
33
environmental_impact/algorithms/dummy_calculator.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
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())
|
6
environmental_impact/apps.py
Normal file
6
environmental_impact/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EnvironmentalImpactConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "environmental_impact"
|
0
environmental_impact/migrations/__init__.py
Normal file
0
environmental_impact/migrations/__init__.py
Normal file
8
environmental_impact/models.py
Normal file
8
environmental_impact/models.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from dataclasses import dataclass
|
||||
from django.db import models
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvironmentalImpact:
|
||||
carbon_saved: float = 0.0
|
||||
co2_emissions: float = 0.0
|
0
environmental_impact/tests/__init__.py
Normal file
0
environmental_impact/tests/__init__.py
Normal file
44
environmental_impact/tests/test_dummy_calculator.py
Normal file
44
environmental_impact/tests/test_dummy_calculator.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
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")
|
|
@ -0,0 +1,17 @@
|
|||
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')
|
3
environmental_impact/views.py
Normal file
3
environmental_impact/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
Loading…
Reference in a new issue