diff --git a/device/views.py b/device/views.py index 13bb273..9e9c542 100644 --- a/device/views.py +++ b/device/views.py @@ -14,7 +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.calculator import get_device_environmental_impact +from environmental_impact.algorithms.algorithm_factory import FactoryEnvironmentImpactAlgorithm if settings.DPP: from dpp.models import Proof from dpp.api_dlt import PROOF_TYPE @@ -111,11 +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': get_device_environmental_impact(self.object), + 'impact': enviromental_impact, 'dpps': dpps, }) return context diff --git a/environmental_impact/algorithms/__init__.py b/environmental_impact/algorithms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/environmental_impact/algorithms/algorithm_factory.py b/environmental_impact/algorithms/algorithm_factory.py new file mode 100644 index 0000000..64c09f1 --- /dev/null +++ b/environmental_impact/algorithms/algorithm_factory.py @@ -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())) diff --git a/environmental_impact/algorithms/algorithm_interface.py b/environmental_impact/algorithms/algorithm_interface.py new file mode 100644 index 0000000..dcca8a5 --- /dev/null +++ b/environmental_impact/algorithms/algorithm_interface.py @@ -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 diff --git a/environmental_impact/algorithms/dummy_calculator.py b/environmental_impact/algorithms/dummy_calculator.py new file mode 100644 index 0000000..6da3d89 --- /dev/null +++ b/environmental_impact/algorithms/dummy_calculator.py @@ -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()) diff --git a/environmental_impact/calculator.py b/environmental_impact/calculator.py deleted file mode 100644 index a348b6b..0000000 --- a/environmental_impact/calculator.py +++ /dev/null @@ -1,30 +0,0 @@ -from dataclasses import dataclass -from device.models import Device - - -@dataclass -class EnvironmentalImpact: - carbon_saved: float = 0.0 - co2_emissions: float = 0.0 - - -def get_device_environmental_impact(device: Device) -> EnvironmentalImpact: - avg_watts = 40 # Arbitrary laptop average consumption - power_on_hours = get_power_on_hours_from(device) - energy_kwh = (power_on_hours * avg_watts) / 1000 - # CO2 emissions based on global average electricity mix - co2_per_kwh = 0.475 - co2_emissions = energy_kwh * co2_per_kwh - return EnvironmentalImpact(co2_emissions=co2_emissions) - - -def get_power_on_hours_from(device: Device) -> int: - storage_components = device.components[9] - str_time = storage_components.get('time of used', -1) - uptime_in_hours = convert_str_time_to_hours(str_time) - return uptime_in_hours - - -def convert_str_time_to_hours(time_str: str) -> int: - multipliers = {'y': 365 * 24, 'd': 24, 'h': 1} - return sum(int(part[:-1]) * multipliers[part[-1]] for part in time_str.split()) diff --git a/environmental_impact/models.py b/environmental_impact/models.py index 71a8362..871c8b6 100644 --- a/environmental_impact/models.py +++ b/environmental_impact/models.py @@ -1,3 +1,8 @@ +from dataclasses import dataclass from django.db import models -# Create your models here. + +@dataclass +class EnvironmentalImpact: + carbon_saved: float = 0.0 + co2_emissions: float = 0.0 diff --git a/environmental_impact/tests.py b/environmental_impact/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/environmental_impact/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/environmental_impact/tests/__init__.py b/environmental_impact/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/environmental_impact/tests/test_dummy_calculator.py b/environmental_impact/tests/test_dummy_calculator.py new file mode 100644 index 0000000..5ee9da6 --- /dev/null +++ b/environmental_impact/tests/test_dummy_calculator.py @@ -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") diff --git a/environmental_impact/tests/test_factory_env_impact_algorithm.py b/environmental_impact/tests/test_factory_env_impact_algorithm.py new file mode 100644 index 0000000..71cab35 --- /dev/null +++ b/environmental_impact/tests/test_factory_env_impact_algorithm.py @@ -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')