From ab9e2cde1abf4e1e97eaeba6a21a0f201fd890c6 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Wed, 11 Feb 2026 08:31:41 +0100 Subject: [PATCH 01/11] docs: source if yearly carbon intensity per country --- install/data/carbon_intensity/carbon-intensity-electricity.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 install/data/carbon_intensity/carbon-intensity-electricity.txt diff --git a/install/data/carbon_intensity/carbon-intensity-electricity.txt b/install/data/carbon_intensity/carbon-intensity-electricity.txt new file mode 100644 index 00000000..a776620c --- /dev/null +++ b/install/data/carbon_intensity/carbon-intensity-electricity.txt @@ -0,0 +1,2 @@ +Data source: +https://ourworldindata.org/grapher/carbon-intensity-electricity \ No newline at end of file From 0926027e60c19f65ade8bf74c3cd2dee34e4ddc9 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Wed, 11 Feb 2026 10:52:59 +0100 Subject: [PATCH 02/11] feat(Embodied\Impact): support for all criterias of Boaviztapi --- install/mysql/plugin_carbon_empty.sql | 60 +++++++-- .../Embodied/Boavizta/AbstractAsset.php | 116 ++++++++++++++++-- src/Impact/Embodied/Boavizta/Computer.php | 1 + src/Impact/Embodied/Engine.php | 6 + src/Impact/Type.php | 48 +++++++- 5 files changed, 200 insertions(+), 31 deletions(-) diff --git a/install/mysql/plugin_carbon_empty.sql b/install/mysql/plugin_carbon_empty.sql index da2f1360..03eacbe1 100644 --- a/install/mysql/plugin_carbon_empty.sql +++ b/install/mysql/plugin_carbon_empty.sql @@ -132,17 +132,51 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_carbon_computerusageprofiles` ( CREATE TABLE IF NOT EXISTS `glpi_plugin_carbon_embodiedimpacts` ( `id` int unsigned NOT NULL AUTO_INCREMENT, - `itemtype` varchar(255) DEFAULT NULL, + `itemtype` varchar(255) DEFAULT NULL, `items_id` int unsigned NOT NULL DEFAULT '0', - `engine` varchar(255) DEFAULT NULL, - `engine_version` varchar(255) DEFAULT NULL, - `date_mod` timestamp NULL DEFAULT NULL, - `gwp` float unsigned DEFAULT '0' COMMENT '(unit gCO2eq) Global warming potential', + `engine` varchar(255) DEFAULT NULL, + `engine_version` varchar(255) DEFAULT NULL, + `date_mod` timestamp NULL DEFAULT NULL, + `gwp` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Global warming potential', `gwp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `adp` float unsigned DEFAULT '0' COMMENT '(unit gSbeq) Abiotic depletion potential', + `adp` float unsigned DEFAULT '0' COMMENT '(unit g Sb eq) Abiotic depletion potential', `adp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `pe` float unsigned DEFAULT '0' COMMENT '(unit J) Primary energy', + `pe` float unsigned DEFAULT '0' COMMENT '(unit J) Primary energy', `pe_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `gwppb` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of biogenic emissions', + `gwppb_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `gwppf` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of fossil fuel emissions', + `gwppf_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `gwpplu` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of emissions from land use change', + `gwpplu_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `ir` float unsigned DEFAULT '0' COMMENT '(unit g U235 eq) Emissions of radionizing substances', + `ir_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `lu` float unsigned DEFAULT '0' COMMENT '(unit none) Land use', + `lu_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `odp` float unsigned DEFAULT '0' COMMENT '(unit g CFC-11 eq) Depletion of the ozone layer', + `odp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `pm` float unsigned DEFAULT '0' COMMENT '(unit Disease occurrence) Fine particle emissions', + `pm_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `pocp` float unsigned DEFAULT '0' COMMENT '(unit g NMVOC eq) Photochemical ozone formation', + `pocp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `wu` float unsigned DEFAULT '0' COMMENT '(unit L) Use of water resources', + `wu_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `mips` float unsigned DEFAULT '0' COMMENT '(unit g) Material input per unit of service', + `mips_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `adpe` float unsigned DEFAULT '0' COMMENT '(unit g SB eq) Use of mineral and metal resources', + `adpe_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `adpf` float unsigned DEFAULT '0' COMMENT '(unit J) Use of fossil resources (including nuclear)', + `adpf_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `ap` float unsigned DEFAULT '0' COMMENT '(unit mol H+ eq) Acidification', + `ap_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `ctue` float unsigned DEFAULT '0' COMMENT '(unit CTUe) Freshwater ecotoxicity', + `ctue_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `epf` float unsigned DEFAULT '0' COMMENT '(unit g P eq) Eutrophication of freshwater', + `epf_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `epm` float unsigned DEFAULT '0' COMMENT '(unit g N eq) Eutrophication of marine waters', + `epm_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `ept` float unsigned DEFAULT '0' COMMENT '(unit mol N eq) Terrestrial eutrophication', + `ept_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', PRIMARY KEY (`id`), UNIQUE KEY `unicity` (`itemtype`, `items_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -161,12 +195,12 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_carbon_monitormodels` ( `gwp` int DEFAULT '0' COMMENT '(unit gCO2eq) Global warming potential', `gwp_source` mediumtext DEFAULT NULL COMMENT 'any information to describe the source, URL preferred', `gwp_quality` int DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `adp` int DEFAULT '0' COMMENT '(unit gSbEq) Abiotic depletion potential', - `adp_source` mediumtext DEFAULT NULL COMMENT 'any information to describe the source, URL preferred', - `adp_quality` int DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `pe` int DEFAULT '0' COMMENT '(unit J) Primary energy', - `pe_source` mediumtext DEFAULT NULL COMMENT 'any information to describe the source, URL preferred', - `pe_quality` int DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `adp` int DEFAULT '0' COMMENT '(unit gSbEq) Abiotic depletion potential', + `adp_source` mediumtext DEFAULT NULL COMMENT 'any information to describe the source, URL preferred', + `adp_quality` int DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + `pe` int DEFAULT '0' COMMENT '(unit J) Primary energy', + `pe_source` mediumtext DEFAULT NULL COMMENT 'any information to describe the source, URL preferred', + `pe_quality` int DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', PRIMARY KEY (`id`), UNIQUE KEY `unicity` (`monitormodels_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/Impact/Embodied/Boavizta/AbstractAsset.php b/src/Impact/Embodied/Boavizta/AbstractAsset.php index f6db53a6..f215f3dc 100644 --- a/src/Impact/Embodied/Boavizta/AbstractAsset.php +++ b/src/Impact/Embodied/Boavizta/AbstractAsset.php @@ -55,6 +55,30 @@ abstract class AbstractAsset extends AbstractEmbodiedImpact implements AssetInte /** @var Client instance of the HTTP client */ protected ?Client $client = null; + protected array $criterias = [ + 'gwp' => 1000, // Kg + 'adp' => 1000, // Kg + 'pe' => 1000000, // MJ + 'gwppb' => 1000, // Kg + 'gwppf' => 1000, // Kg + 'gwpplu' => 1000, // Kg + 'ir' => 1000, // Kg + 'lu' => 1, // (no unit) + 'odp' => 1000, // Kg + 'pm' => 1, // (no unit) + 'pocp' => 1000, // Kg + 'wu' => 1000, // M^3 + 'mips' => 1000, // Kg + 'adpe' => 1000, // Kg + 'adpf' => 1000000, // MJ + 'ap' => 1, // mol + 'ctue' => 1, // CTUe + // 'ctuh_c' => '', // CTUh request fails when this criteria is added, not a URL encoding issue + 'epf' => 1000, // Kg + 'epm' => 1000, // Kg + 'ept' => 1, // mol + ]; + // abstract public static function getEngine(CommonDBTM $item): EngineInterface; /** @@ -98,7 +122,23 @@ protected function getVersion(): string return self::$engine_version; } - protected function query($description): array + /** + * Get the quety string specifying the impact criterias for the HTTP request + * + * @return string + */ + protected function getCriteriasQueryString(): string + { + return 'criteria=' . implode('&criteria=', array_keys($this->criterias)); + } + + /** + * Send a HTTP query + * + * @param array $description + * @return array + */ + protected function query(array $description): array { try { $response = $this->client->post($this->endpoint, [ @@ -115,6 +155,7 @@ protected function query($description): array /** * Read the response to find the impacts provided by Boaviztapi * + * @param array $response * @return array */ protected function parseResponse(array $response): array @@ -130,22 +171,73 @@ protected function parseResponse(array $response): array if ($impact_id === false) { continue; } - switch ($type) { - case 'gwp': - $impacts[$impact_id] = $this->parseGwp($response['impacts'][$type]); - break; - case 'adp': - $impacts[$impact_id] = $this->parseAdp($response['impacts'][$type]); - break; - case 'pe': - $impacts[$impact_id] = $this->parsePe($response['impacts'][$type]); - break; - } + $impacts[$impact_id] = $this->parseCriteria($type, $response['impacts'][$type]); + // switch ($type) { + // case 'gwp': + // $impacts[$impact_id] = $this->parseGwp($response['impacts'][$type]); + // break; + // case 'adp': + // $impacts[$impact_id] = $this->parseAdp($response['impacts'][$type]); + // break; + // case 'pe': + // $impacts[$impact_id] = $this->parsePe($response['impacts'][$type]); + // break; + // case 'gwppb': + // break; + // case 'gwppf': + // break; + // case 'gwpplu': + // break; + // case 'ir': + // break; + // case 'lu': + // break; + // case 'odp': + // break; + // case 'pm': + // break; + // case 'pocp': + // break; + // case 'wu': + // break; + // case 'mips': + // break; + // case 'adpe': + // break; + // case 'adpf': + // break; + // case 'ap': + // break; + // case 'ctue': + // break; + // case 'epf': + // break; + // case 'epm': + // break; + // case 'ept': + // break; + // } } return $impacts; } + protected function parseCriteria(string $name, array $impact): ?TrackedFloat + { + if ($impact['embedded'] === 'not implemented') { + return null; + } + + $unit_multiplier = $this->criterias[$name]; + $value = new TrackedFloat( + $impact['embedded']['value'] * $unit_multiplier, + null, + TrackedFloat::DATA_QUALITY_ESTIMATED + ); + + return $value; + } + protected function parseGwp(array $impact): ?TrackedFloat { if ($impact['embedded'] === 'not implemented') { diff --git a/src/Impact/Embodied/Boavizta/Computer.php b/src/Impact/Embodied/Boavizta/Computer.php index f395664e..12e6e4b8 100644 --- a/src/Impact/Embodied/Boavizta/Computer.php +++ b/src/Impact/Embodied/Boavizta/Computer.php @@ -59,6 +59,7 @@ protected function doEvaluation(): ?array // adapt $this->endpoint depending on the type of computer (server, laptop, ...) $type = $this->getType($this->item); $this->endpoint = $this->getEndpoint($type); + $this->endpoint .= '?' . $this->getCriteriasQueryString(); // Ask for embodied impact only $handle_hardware = in_array($type, [ diff --git a/src/Impact/Embodied/Engine.php b/src/Impact/Embodied/Engine.php index 7a3a2dbb..c033983f 100644 --- a/src/Impact/Embodied/Engine.php +++ b/src/Impact/Embodied/Engine.php @@ -131,6 +131,8 @@ protected static function configureEngine(EmbodiedImpactInterface $engine): Embo */ private static function hasModelData(CommonDBTM $item): bool { + global $DB; + $itemtype = get_class($item); $glpi_model_class = $itemtype . 'Model'; $glpi_model_class_fk = getForeignKeyFieldForItemType($glpi_model_class); @@ -138,12 +140,16 @@ private static function hasModelData(CommonDBTM $item): bool * @var class-string $model_class */ $model_class = 'GlpiPlugin\\Carbon\\' . $glpi_model_class; + $model_table = getTableForItemType($model_class); $glpi_model_id = $item->fields[$glpi_model_class_fk]; $crit = [ $glpi_model_class_fk => $glpi_model_id ]; $types = Type::getImpactTypes(); foreach ($types as $key => $type) { + if (!$DB->fieldExists($model_table, $type)) { + continue; + } $crit['OR'][] = [ $type . '_quality' => ['<>', AbstractTracked::DATA_QUALITY_UNSET_VALUE] ]; diff --git a/src/Impact/Type.php b/src/Impact/Type.php index 7c3f5551..7a9de684 100644 --- a/src/Impact/Type.php +++ b/src/Impact/Type.php @@ -35,14 +35,50 @@ class Type { - const IMPACT_GWP = 0; // Global warming potential - const IMPACT_ADP = 1; // Abiotic Depletion Potential - const IMPACT_PE = 2; // Primary Energy + const IMPACT_GWP = 0; // Global warming potential + const IMPACT_ADP = 1; // Abiotic Depletion Potential + const IMPACT_PE = 3; // Primary Energy + const IMPACT_GWPPB = 4; + const IMPACT_GWPPF = 5; + const IMPACT_GWPPLU = 6; + const IMPACT_IR = 7; + const IMPACT_LU = 8; + const IMPACT_ODP = 9; + const IMPACT_PM = 10; + const IMPACT_POCP = 11; + const IMPACT_WU = 12; + const IMPACT_MIPS = 13; + const IMPACT_ADPE = 14; + const IMPACT_ADPF = 15; + const IMPACT_AP = 16; + const IMPACT_CTUE = 17; + // const IMPACT_CTUHC = 18; + const IMPACT_EPF = 19; + const IMPACT_EPM = 20; + const IMPACT_EPT = 21; private static array $impact_types = [ - self::IMPACT_GWP => 'gwp', - self::IMPACT_ADP => 'adp', - self::IMPACT_PE => 'pe', + self::IMPACT_GWP => 'gwp', + self::IMPACT_ADP => 'adp', + self::IMPACT_PE => 'pe', + self::IMPACT_GWPPB => 'gwppb', + self::IMPACT_GWPPF => 'gwppf', + self::IMPACT_GWPPLU => 'gwpplu', + self::IMPACT_IR => 'ir', + self::IMPACT_LU => 'lu', + self::IMPACT_ODP => 'odp', + self::IMPACT_PM => 'pm', + self::IMPACT_POCP => 'pocp', + self::IMPACT_WU => 'wu', + self::IMPACT_MIPS => 'mips', + self::IMPACT_ADPE => 'adpe', + self::IMPACT_ADPF => 'adpf', + self::IMPACT_AP => 'ap', + self::IMPACT_CTUE => 'ctue', + // self::IMPACT_CTUHC => 'ctuh_c', + self::IMPACT_EPF => 'epf', + self::IMPACT_EPM => 'epm', + self::IMPACT_EPT => 'ept', ]; public static function getImpactTypes(): array From ad2a1e9307be58e299482201b65f75c0fa5bc752 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 08:54:30 +0100 Subject: [PATCH 03/11] feat(Embodied\Impact): cards for all embodied impacts --- .../09_add_impact_criterias.php | 118 +++++++++++++++++ src/Dashboard/Grid.php | 123 ++++++++++++------ src/Dashboard/Provider.php | 39 ++++++ src/EmbodiedImpact.php | 4 +- .../Embodied/Boavizta/AbstractAsset.php | 55 +------- src/Impact/Type.php | 121 ++++++++++++++++- src/Toolbox.php | 77 +++++++---- 7 files changed, 423 insertions(+), 114 deletions(-) create mode 100644 install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php diff --git a/install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php b/install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php new file mode 100644 index 00000000..4d718d4b --- /dev/null +++ b/install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php @@ -0,0 +1,118 @@ +. + * + * ------------------------------------------------------------------------- + */ + +/** @var DBmysql $DB */ +/** @var Migration $migration */ + +use Glpi\Dashboard\Item as DashboardItem; + +$new_criterias = [ + 'gwppb' => '(unit g CO2 eq) Climate change - Contribution of biogenic emissions', + 'gwppf' => '(unit g CO2 eq) Climate change - Contribution of fossil fuel emissions', + 'gwpplu' => '(unit g CO2 eq) Climate change - Contribution of emissions from land use change', + 'ir' => '(unit g U235 eq) Emissions of radionizing substances', + 'lu' => '(unit none) Land use', + 'odp' => '(unit g CFC-11 eq) Depletion of the ozone layer', + 'pm' => '(unit Disease occurrence) Fine particle emissions', + 'pocp' => '(unit g NMVOC eq) Photochemical ozone formation', + 'wu' => '(unit L) Use of water resources', + 'mips' => '(unit g) Material input per unit of service', + 'adpe' => '(unit g SB eq) Use of mineral and metal resources', + 'adpf' => '(unit J) Use of fossil resources (including nuclear)', + 'ap' => '(unit mol H+ eq) Acidification', + 'ctue' => '(unit CTUe) Freshwater ecotoxicity', + // ctuh_c => '(unit CTUh) Human toxicity - non-carcinogenic effects', + 'epf' => '(unit g P eq) Eutrophication of freshwater', + 'epm' => '(unit g N eq) Eutrophication of marine waters', + 'ept' => '(unit mol N eq) Terrestrial eutrophication', +]; + +$table = 'glpi_plugin_carbon_embodiedimpacts'; +$previous_criteria = 'pe'; +foreach ($new_criterias as $criteria => $comment) { + $migration->addField( + $table, + $criteria, + 'float DEFAULT \'0\'', + [ + 'comment' => $comment, + 'after' => $previous_criteria . '_quality', + ] + ); + $migration->addField( + $table, + $criteria . '_quality', + 'int unsigned NOT NULL DEFAULT \'0\'', + [ + 'comment' => 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', + 'after' => $criteria, + ] + ); + // $migration->dropField($table, $criteria); + // $migration->dropField($table, $criteria . '_quality'); + $previous_criteria = $criteria; +} + +// Uniformize existing impact : make floats signed +$old_criterias = [ + 'gwp' => '(unit g CO2 eq) Global warming potential', + 'adp' => '(unit g Sb eq) Abiotic depletion potential', + 'pe' => '(unit J) Primary energy', +]; +foreach ($old_criterias as $criteria => $comment) { + $migration->changeField($table, $criteria, $criteria, 'float DEFAULT \'0\'', [ + 'comment' => $comment, + ]); +} + +// Rename cards for the report +$dashboard_item = new DashboardItem(); +$rows = $dashboard_item->find([ + 'card_id' => 'plugin_carbon_report_embodied_gwp_impact' +]); +foreach ($rows as $row) { + $dashboard_item->update([ + 'id' => $row['id'], + 'card_id' => 'plugin_carbon_report_embodied_gwp_impact', + ]); +} + +$dashboard_item = new DashboardItem(); +$rows = $dashboard_item->find([ + 'card_id' => 'plugin_carbon_report_embodied_abiotic_depletion' +]); +foreach ($rows as $row) { + $dashboard_item->update([ + 'id' => $row['id'], + 'card_id' => 'plugin_carbon_report_embodied_adp_impact', + ]); +} diff --git a/src/Dashboard/Grid.php b/src/Dashboard/Grid.php index 3dc30e84..879990a6 100644 --- a/src/Dashboard/Grid.php +++ b/src/Dashboard/Grid.php @@ -35,6 +35,7 @@ use Computer; use Glpi\Dashboard\Filter; use GlpiPlugin\Carbon\Config; +use GlpiPlugin\Carbon\Impact\Type; use Session; class Grid @@ -153,6 +154,22 @@ protected static function getStandardCards(): array ]; } + foreach (Type::getImpactTypes() as $impact_type) { + $key = "plugin_carbon_embodied_{$impact_type}_impact"; + if (isset($new_cards[$key])) { + trigger_error("The card $key already exists", E_USER_WARNING); + } + $new_cards[$key] = [ + 'widgettype' => ['bigNumber'], + 'group' => $group, + 'label' => Type::getEmbodiedImpactLabel($impact_type), + 'provider' => Provider::class . '::getImpactOfEmbodiedCriteria', + 'args' => [ + 'impact_type' => $impact_type + ] + ]; + } + $new_cards += [ // Usage impact 'plugin_carbon_total_usage_power' => [ @@ -175,24 +192,24 @@ protected static function getStandardCards(): array ], // Embodied impact - 'plugin_carbon_embodied_gwp_impact' => [ - 'widgettype' => ['bigNumber'], - 'group' => $group, - 'label' => __('Embodied global warming potential', 'carbon'), - 'provider' => Provider::class . '::getEmbodiedGlobalWarming', - ], - 'plugin_carbon_embodied_pe_impact' => [ - 'widgettype' => ['bigNumber'], - 'group' => $group, - 'label' => __('Embodied primary energy consumed', 'carbon'), - 'provider' => Provider::class . '::getEmbodiedPrimaryEnergy', - ], - 'plugin_carbon_embodied_adp_impact' => [ - 'widgettype' => ['bigNumber'], - 'group' => $group, - 'label' => __('Embodied abiotic depletion potential', 'carbon'), - 'provider' => Provider::class . '::getEmbodiedAbioticDepletion', - ], + // 'plugin_carbon_embodied_gwp_impact' => [ + // 'widgettype' => ['bigNumber'], + // 'group' => $group, + // 'label' => __('Embodied global warming potential', 'carbon'), + // 'provider' => Provider::class . '::getEmbodiedGlobalWarming', + // ], + // 'plugin_carbon_embodied_pe_impact' => [ + // 'widgettype' => ['bigNumber'], + // 'group' => $group, + // 'label' => __('Embodied primary energy consumed', 'carbon'), + // 'provider' => Provider::class . '::getEmbodiedPrimaryEnergy', + // ], + // 'plugin_carbon_embodied_adp_impact' => [ + // 'widgettype' => ['bigNumber'], + // 'group' => $group, + // 'label' => __('Embodied abiotic depletion potential', 'carbon'), + // 'provider' => Provider::class . '::getEmbodiedAbioticDepletion', + // ], // embodied + usage impact 'plugin_carbon_total_gwp_impact' => [ @@ -238,8 +255,8 @@ protected static function getReportCards(): array ]; } - // Usage impact $new_cards += [ + // Usage impact 'plugin_carbon_report_usage_carbon_emission_ytd' => [ 'widgettype' => ['usage_carbon_emission_ytd'], 'group' => $group, @@ -278,25 +295,40 @@ protected static function getReportCards(): array ], // Embodied impact - 'plugin_carbon_report_embodied_global_warming' => [ - 'widgettype' => ['embodied_global_warming'], - 'group' => $group, - 'label' => __('Embodied global warming potential', 'carbon'), - 'provider' => Provider::class . '::getEmbodiedGlobalWarming', - ], - 'plugin_carbon_report_embodied_abiotic_depletion' => [ - 'widgettype' => ['embodied_abiotic_depletion'], - 'group' => $group, - 'label' => __('Embodied abiotic depletion potential', 'carbon'), - 'provider' => Provider::class . '::getEmbodiedAbioticDepletion', - ], - 'plugin_carbon_report_embodied_pe_impact' => [ - 'widgettype' => ['embodied_primary_energy'], - 'group' => $group, - 'label' => __('Embodied primary energy consumed', 'carbon'), - 'provider' => Provider::class . '::getEmbodiedPrimaryEnergy', - ], + // 'plugin_carbon_report_embodied_global_warming' => [ + // 'widgettype' => ['embodied_global_warming'], + // 'group' => $group, + // 'label' => __('Embodied global warming potential', 'carbon'), + // 'provider' => Provider::class . '::getEmbodiedGlobalWarming', + // ], + // 'plugin_carbon_report_embodied_abiotic_depletion' => [ + // 'widgettype' => ['embodied_abiotic_depletion'], + // 'group' => $group, + // 'label' => __('Embodied abiotic depletion potential', 'carbon'), + // 'provider' => Provider::class . '::getEmbodiedAbioticDepletion', + // ], + // 'plugin_carbon_report_embodied_pe_impact' => [ + // 'widgettype' => ['embodied_primary_energy'], + // 'group' => $group, + // 'label' => __('Embodied primary energy consumed', 'carbon'), + // 'provider' => Provider::class . '::getEmbodiedPrimaryEnergy', + // ], ]; + foreach (Type::getImpactTypes() as $impact_type) { + $key = "plugin_carbon_report_embodied_{$impact_type}_impact"; + if (isset($new_cards[$key])) { + trigger_error("The card $key already exists", E_USER_WARNING); + } + $new_cards[$key] = [ + 'widgettype' => [self::getWidgetForImpact(true, $impact_type)], + 'group' => $group, + 'label' => Type::getEmbodiedImpactLabel($impact_type), + 'provider' => Provider::class . '::getImpactOfEmbodiedCriteria', + 'args' => [ + 'impact_type' => $impact_type + ] + ]; + } // Informational content $new_cards += [ @@ -314,4 +346,21 @@ protected static function getReportCards(): array return $new_cards; } + + private static function getWidgetForImpact(bool $embodied, string $type): string + { + switch (($embodied ? 'embodied' : 'usage') . ' ' . $type) { + case 'embodied gwp': + return 'embodied_global_warming'; + break; + case 'embodied adp': + return 'embodied_abiotic_depletion'; + break; + case 'embodied pe': + return 'embodied_primary_energy'; + break; + } + + return ''; + } } diff --git a/src/Dashboard/Provider.php b/src/Dashboard/Provider.php index 1e8f188e..16d830c3 100644 --- a/src/Dashboard/Provider.php +++ b/src/Dashboard/Provider.php @@ -53,6 +53,7 @@ use GlpiPlugin\Carbon\UsageImpact; use Glpi\DBAL\QueryExpression; use Glpi\DBAL\QuerySubQuery; +use GlpiPlugin\Carbon\Impact\Type; use Search; use Session; use Toolbox as GlpiToolbox; @@ -950,6 +951,44 @@ public static function getEmbodiedAbioticDepletion(array $params = [], array $cr ]; } + /** + * Total embodied abiotic depletion potential in antimony equivalent + * + * @param array $params + * @param array $crit + * @return array + */ + public static function getImpactOfEmbodiedCriteria(string $impact_type, array $params = [], array $crit = []): array + { + $default_params = [ + 'label' => Type::getEmbodiedImpactLabel($impact_type), + 'icon' => 'fa-solid fa-temperature-arrow-up', + ]; + $params = array_merge($default_params, $params); + if (count($crit['itemtype'] ?? []) === 0) { + $crit['itemtype'] = PLUGIN_CARBON_TYPES; + } else { + $crit['itemtype'] = array_intersect($crit['itemtype'], PLUGIN_CARBON_TYPES); + } + + $value = self::getSum(EmbodiedImpact::getTable(), $impact_type, $params, $crit); + if ($value === null) { + $value = 'N/A'; + } else { + // $value = Toolbox::getWeight($value) . Type::getImpactUnit($impact_type); + $value = Toolbox::getHumanReadableValue( + $value, + Type::getImpactUnit($impact_type) + ); + } + + return [ + 'number' => $value, + 'label' => $params['label'], + 'icon' => $params['icon'], + ]; + } + /** * Get usage abiotic depletion potential in antimony equivalent * diff --git a/src/EmbodiedImpact.php b/src/EmbodiedImpact.php index 13271feb..190ece8f 100644 --- a/src/EmbodiedImpact.php +++ b/src/EmbodiedImpact.php @@ -90,7 +90,7 @@ public function rawSearchOptions() 'name' => __('Global Warming Potential', 'carbon'), 'massiveaction' => false, 'datatype' => 'number', - 'unit' => 'gCO2eq', + 'unit' => 'g CO2 eq', ]; $tab[] = [ @@ -100,7 +100,7 @@ public function rawSearchOptions() 'name' => __('Abiotic Depletion Potential', 'carbon'), 'massiveaction' => false, 'datatype' => 'number', - 'unit' => 'KgSbeq', + 'unit' => 'g Sb eq', ]; $tab[] = [ diff --git a/src/Impact/Embodied/Boavizta/AbstractAsset.php b/src/Impact/Embodied/Boavizta/AbstractAsset.php index f215f3dc..e7f9685d 100644 --- a/src/Impact/Embodied/Boavizta/AbstractAsset.php +++ b/src/Impact/Embodied/Boavizta/AbstractAsset.php @@ -55,7 +55,8 @@ abstract class AbstractAsset extends AbstractEmbodiedImpact implements AssetInte /** @var Client instance of the HTTP client */ protected ?Client $client = null; - protected array $criterias = [ + /** @var array Supported impact criterias and the multiplier unit of the value returned by Boaviztapi */ + protected array $criteria_units = [ 'gwp' => 1000, // Kg 'adp' => 1000, // Kg 'pe' => 1000000, // MJ @@ -73,7 +74,8 @@ abstract class AbstractAsset extends AbstractEmbodiedImpact implements AssetInte 'adpf' => 1000000, // MJ 'ap' => 1, // mol 'ctue' => 1, // CTUe - // 'ctuh_c' => '', // CTUh request fails when this criteria is added, not a URL encoding issue + // 'ctuh_c' => 1, // CTUh request fails when this criteria is added, not a URL encoding issue + // 'ctuh_nc' => 1, // CTUh request fails when this criteria is added, not a URL encoding issue 'epf' => 1000, // Kg 'epm' => 1000, // Kg 'ept' => 1, // mol @@ -129,7 +131,7 @@ protected function getVersion(): string */ protected function getCriteriasQueryString(): string { - return 'criteria=' . implode('&criteria=', array_keys($this->criterias)); + return 'criteria=' . implode('&criteria=', array_keys($this->criteria_units)); } /** @@ -172,51 +174,6 @@ protected function parseResponse(array $response): array continue; } $impacts[$impact_id] = $this->parseCriteria($type, $response['impacts'][$type]); - // switch ($type) { - // case 'gwp': - // $impacts[$impact_id] = $this->parseGwp($response['impacts'][$type]); - // break; - // case 'adp': - // $impacts[$impact_id] = $this->parseAdp($response['impacts'][$type]); - // break; - // case 'pe': - // $impacts[$impact_id] = $this->parsePe($response['impacts'][$type]); - // break; - // case 'gwppb': - // break; - // case 'gwppf': - // break; - // case 'gwpplu': - // break; - // case 'ir': - // break; - // case 'lu': - // break; - // case 'odp': - // break; - // case 'pm': - // break; - // case 'pocp': - // break; - // case 'wu': - // break; - // case 'mips': - // break; - // case 'adpe': - // break; - // case 'adpf': - // break; - // case 'ap': - // break; - // case 'ctue': - // break; - // case 'epf': - // break; - // case 'epm': - // break; - // case 'ept': - // break; - // } } return $impacts; @@ -228,7 +185,7 @@ protected function parseCriteria(string $name, array $impact): ?TrackedFloat return null; } - $unit_multiplier = $this->criterias[$name]; + $unit_multiplier = $this->criteria_units[$name]; $value = new TrackedFloat( $impact['embedded']['value'] * $unit_multiplier, null, diff --git a/src/Impact/Type.php b/src/Impact/Type.php index 7a9de684..78c7bd0a 100644 --- a/src/Impact/Type.php +++ b/src/Impact/Type.php @@ -53,9 +53,10 @@ class Type const IMPACT_AP = 16; const IMPACT_CTUE = 17; // const IMPACT_CTUHC = 18; - const IMPACT_EPF = 19; - const IMPACT_EPM = 20; - const IMPACT_EPT = 21; + // const IMPACT_CTUHNC = 19; + const IMPACT_EPF = 20; + const IMPACT_EPM = 21; + const IMPACT_EPT = 22; private static array $impact_types = [ self::IMPACT_GWP => 'gwp', @@ -76,11 +77,42 @@ class Type self::IMPACT_AP => 'ap', self::IMPACT_CTUE => 'ctue', // self::IMPACT_CTUHC => 'ctuh_c', + // self::IMPACT_CTUHNC => 'ctuh_nc', self::IMPACT_EPF => 'epf', self::IMPACT_EPM => 'epm', self::IMPACT_EPT => 'ept', ]; + /** + * Unit of impact criterias + * + * @var array + */ + private static array $impact_units = [ + 'gwp' => ['g', 'CO2 eq'], + 'adp' => ['g', 'SB eq'], + 'pe' => ['J', ''], + 'gwppb' => ['g', 'CO2 eq'], + 'gwppf' => ['g', 'CO2 eq'], + 'gwpplu' => ['g', 'CO2 eq'], + 'ir' => ['g', 'U235 eq'], + 'lu' => null, + 'odp' => ['g', 'CFC-11 eq'], + 'pm' => null, + 'pocp' => ['g', 'U235 eq'], + 'wu' => ['m³', ''], + 'mips' => ['g', ''], + 'adpe' => ['g', 'SB eq'], + 'adpf' => ['J', ''], + 'ap' => ['mol', 'H+ eq'], + 'ctue' => ['J', ''], + // 'ctuh_c' => [null, 'CTUh'], + // 'ctuh_nc' => [null, 'CTUh'], + 'epf' => ['g', 'P eq'], + 'epm' => ['g', 'N eq'], + 'ept' => ['mol', 'N eq'], + ]; + public static function getImpactTypes(): array { return self::$impact_types; @@ -91,9 +123,90 @@ public static function getImpactTypes(): array * * @param string $type * @return int|string|false - */ + **/ public static function getImpactId(string $type) { return array_search($type, self::$impact_types); } + + /** + * Get the unit of an impact type + * + * @param string $type impact type name + * @return array + **/ + public static function getImpactUnit(string $type): array + { + return self::$impact_units[$type] ?? ['', '']; + } + + /** + * Get the unit of an impact type + * + * @param string $type impact type name + * @return string + **/ + public static function getEmbodiedImpactLabel(string $type): string + { + $embodied_impact_label = [ + 'gwp' => __('Embodied Global warming potential', 'carbon'), + 'adp' => __('Embodied Abiotic depletion potential', 'carbon'), + 'pe' => __('Embodied Primary energy consumed', 'carbon'), + 'gwppb' => __('Embodied Climate change - Contribution of biogenic emissions', 'carbon'), + 'gwppf' => __('Embodied Climate change - Contribution of fossil fuel emissions', 'carbon'), + 'gwpplu' => __('Embodied Climate change - Contribution of emissions from land use change', 'carbon'), + 'ir' => __('Embodied Emissions of radionizing substances', 'carbon'), + 'lu' => __('Embodied Land use', 'carbon'), + 'odp' => __('Embodied Depletion of the ozone layer', 'carbon'), + 'pm' => __('Embodied Fine particle emissions', 'carbon'), + 'pocp' => __('Embodied Photochemical ozone formation', 'carbon'), + 'wu' => __('Embodied Use of water resources', 'carbon'), + 'mips' => __('Embodied Material input per unit of service', 'carbon'), + 'adpe' => __('Embodied Use of mineral and metal resources', 'carbon'), + 'adpf' => __('Embodied Use of fossil resources (including nuclear)', 'carbon'), + 'ap' => __('Embodied Acidification', 'carbon'), + 'ctue' => __('Embodied Human Toxicity - Carcinogenic Effects', 'carbon'), + // 'ctuh_c' => __('Embodied Human Toxicity - Carcinogenic Effects', 'carbon'), + // 'ctuh_nc' => __('Embodied Human toxicity - non-carcinogenic effects', 'carbon'), + 'epf' => __('Embodied Eutrophication of freshwater', 'carbon'), + 'epm' => __('Embodied Eutrophication of marine waters', 'carbon'), + 'ept' => __('Embodied Terrestrial eutrophication', 'carbon'), + ]; + return $embodied_impact_label[$type] ?? ''; + } + + /** + * Get the unit of an impact type + * + * @param string $type impact type name + * @return string + **/ + public static function getUsageImpactLabel(string $type): string + { + static $usage_impact_label = [ + 'gwp' => __('Usage Global warming potential', 'carbon'), + 'adp' => __('Usage Abiotic depletion potential', 'carbon'), + 'pe' => __('Usage Primary energy consumed', 'carbon'), + 'gwppb' => __('Usage Climate change - Contribution of biogenic emissions', 'carbon'), + 'gwppf' => __('Usage Climate change - Contribution of fossil fuel emissions', 'carbon'), + 'gwpplu' => __('Usage Climate change - Contribution of emissions from land use change', 'carbon'), + 'ir' => __('Usage Emissions of radionizing substances', 'carbon'), + 'lu' => __('Usage Land use', 'carbon'), + 'odp' => __('Usage Depletion of the ozone layer', 'carbon'), + 'pm' => __('Usage Fine particle emissions', 'carbon'), + 'pocp' => __('Usage Photochemical ozone formation', 'carbon'), + 'wu' => __('Usage Use of water resources', 'carbon'), + 'mips' => __('Usage Material input per unit of service', 'carbon'), + 'adpe' => __('Usage Use of mineral and metal resources', 'carbon'), + 'adpf' => __('Usage Use of fossil resources (including nuclear)', 'carbon'), + 'ap' => __('Usage Acidification', 'carbon'), + 'ctue' => __('Usage Human Toxicity - Carcinogenic Effects', 'carbon'), + // 'ctuh_c' => __('Usage Human Toxicity - Carcinogenic Effects', 'carbon'), + // 'ctuh_nc' => __('Usage Human toxicity - non-carcinogenic effects', 'carbon'), + 'epf' => __('Usage Eutrophication of freshwater', 'carbon'), + 'epm' => __('Usage Eutrophication of marine waters', 'carbon'), + 'ept' => __('Usage Terrestrial eutrophication', 'carbon'), + ]; + return $usage_impact_label[$type] ?? ''; + } } diff --git a/src/Toolbox.php b/src/Toolbox.php index 328e34cf..5dfc8b64 100644 --- a/src/Toolbox.php +++ b/src/Toolbox.php @@ -201,9 +201,9 @@ public static function getWeight(float $weight): string { //TRANS: list of unit (o for octet) $units = [ - __('g', 'carbon'), + __('g', 'carbon'), __('Kg', 'carbon'), - __('t', 'carbon'), + __('t', 'carbon'), __('Kt', 'carbon'), __('Mt', 'carbon'), __('Gt', 'carbon'), @@ -224,20 +224,7 @@ public static function getWeight(float $weight): string $weight = self::dynamicRound($weight); //TRANS: %1$s is a number maybe float or string and %2$s the unit - return sprintf(__('%1$s %2$s'), $weight, $human_readable_unit); - } - - public static function dynamicRound(float $number): float - { - if ($number < 10) { - $number = round($number, 2); - } else if ($number < 100) { - $number = round($number, 1); - } else { - $number = round($number, 0); - } - - return $number; + return sprintf(__('%1$s %2$s'), $weight, $human_readable_unit); } /** @@ -272,19 +259,19 @@ public static function getPower(float $p): string $p = self::dynamicRound($p); //TRANS: %1$s is a number maybe float or string and %2$s the unit - return sprintf(__('%1$s %2$s'), $p, $human_readable_unit); + return sprintf(__('%1$s %2$s'), $p, $human_readable_unit); } /** - * Format a power passing a power in grams + * Format a energy in watt.hour * - * @param float $p Power in Watt + * @param float $p Energy in watt.hour * - * @return string formatted power + * @return string formatted energy **/ public static function getEnergy(float $p): string { - //TRANS: list of unit (W for watt) + //TRANS: list of unit (Wh for watt.hour) $units = [ __('Wh', 'carbon'), __('KWh', 'carbon'), @@ -307,7 +294,53 @@ public static function getEnergy(float $p): string $p = self::dynamicRound($p); //TRANS: %1$s is a number maybe float or string and %2$s the unit - return sprintf(__('%1$s %2$s'), $p, $human_readable_unit); + return sprintf(__('%1$s %2$s'), $p, $human_readable_unit); + } + + public static function dynamicRound(float $number): float + { + if ($number < 10) { + $number = round($number, 2); + } else if ($number < 100) { + $number = round($number, 1); + } else { + $number = round($number, 0); + } + + return $number; + } + + /** + * Convert a value and its unit into a human readable value + * + * Unit is an array i.e. ['g', 'CO2 eq'] for grams of carbondioxyde equivalent + * + * @param float $value value of the quantity to convert + * @param array $unit unit splitted into a standard unit and a qualification. + * @return string + */ + public static function getHumanReadableValue(float $value, array $unit): string + { + switch ($unit[0]) { + case 'g': + return self::getWeight($value) . $unit[1]; + break; + case 'J': + // To be converted into watt.hour + return self::getEnergy($value / 3600) . $unit[1]; + break; + case 'Wh': + return self::getEnergy($value) . $unit[1]; + break; + case 'm³': + // Value is in m^3 + return sprintf(__('%1$s %2$s', 'carbon'), $value * 1000, 'L'); + break; + case 'mol': + break; + } + + return sprintf(__('%1$s %2$s', 'carbon'), $value, implode(' ', $unit)); } /** From 1dd0625d5097d6f2dbc183a65adc6a7bed032fcd Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 09:34:35 +0100 Subject: [PATCH 04/11] feat(Embodied\Impact): add widgets for new impacts --- install/mysql/plugin_carbon_empty.sql | 40 +++++++++++++-------------- src/Impact/Type.php | 14 ++++++---- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/install/mysql/plugin_carbon_empty.sql b/install/mysql/plugin_carbon_empty.sql index 03eacbe1..53f8c35b 100644 --- a/install/mysql/plugin_carbon_empty.sql +++ b/install/mysql/plugin_carbon_empty.sql @@ -137,45 +137,45 @@ CREATE TABLE IF NOT EXISTS `glpi_plugin_carbon_embodiedimpacts` ( `engine` varchar(255) DEFAULT NULL, `engine_version` varchar(255) DEFAULT NULL, `date_mod` timestamp NULL DEFAULT NULL, - `gwp` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Global warming potential', + `gwp` float DEFAULT '0' COMMENT '(unit g CO2 eq) Global warming potential', `gwp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `adp` float unsigned DEFAULT '0' COMMENT '(unit g Sb eq) Abiotic depletion potential', + `adp` float DEFAULT '0' COMMENT '(unit g Sb eq) Abiotic depletion potential', `adp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `pe` float unsigned DEFAULT '0' COMMENT '(unit J) Primary energy', + `pe` float DEFAULT '0' COMMENT '(unit J) Primary energy', `pe_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `gwppb` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of biogenic emissions', + `gwppb` float DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of biogenic emissions', `gwppb_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `gwppf` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of fossil fuel emissions', + `gwppf` float DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of fossil fuel emissions', `gwppf_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `gwpplu` float unsigned DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of emissions from land use change', + `gwpplu` float DEFAULT '0' COMMENT '(unit g CO2 eq) Climate change - Contribution of emissions from land use change', `gwpplu_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `ir` float unsigned DEFAULT '0' COMMENT '(unit g U235 eq) Emissions of radionizing substances', + `ir` float DEFAULT '0' COMMENT '(unit g U235 eq) Emissions of radionizing substances', `ir_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `lu` float unsigned DEFAULT '0' COMMENT '(unit none) Land use', + `lu` float DEFAULT '0' COMMENT '(unit none) Land use', `lu_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `odp` float unsigned DEFAULT '0' COMMENT '(unit g CFC-11 eq) Depletion of the ozone layer', + `odp` float DEFAULT '0' COMMENT '(unit g CFC-11 eq) Depletion of the ozone layer', `odp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `pm` float unsigned DEFAULT '0' COMMENT '(unit Disease occurrence) Fine particle emissions', + `pm` float DEFAULT '0' COMMENT '(unit Disease occurrence) Fine particle emissions', `pm_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `pocp` float unsigned DEFAULT '0' COMMENT '(unit g NMVOC eq) Photochemical ozone formation', + `pocp` float DEFAULT '0' COMMENT '(unit g NMVOC eq) Photochemical ozone formation', `pocp_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `wu` float unsigned DEFAULT '0' COMMENT '(unit L) Use of water resources', + `wu` float DEFAULT '0' COMMENT '(unit L) Use of water resources', `wu_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `mips` float unsigned DEFAULT '0' COMMENT '(unit g) Material input per unit of service', + `mips` float DEFAULT '0' COMMENT '(unit g) Material input per unit of service', `mips_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `adpe` float unsigned DEFAULT '0' COMMENT '(unit g SB eq) Use of mineral and metal resources', + `adpe` float DEFAULT '0' COMMENT '(unit g SB eq) Use of mineral and metal resources', `adpe_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `adpf` float unsigned DEFAULT '0' COMMENT '(unit J) Use of fossil resources (including nuclear)', + `adpf` float DEFAULT '0' COMMENT '(unit J) Use of fossil resources (including nuclear)', `adpf_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `ap` float unsigned DEFAULT '0' COMMENT '(unit mol H+ eq) Acidification', + `ap` float DEFAULT '0' COMMENT '(unit mol H+ eq) Acidification', `ap_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `ctue` float unsigned DEFAULT '0' COMMENT '(unit CTUe) Freshwater ecotoxicity', + `ctue` float DEFAULT '0' COMMENT '(unit CTUe) Freshwater ecotoxicity', `ctue_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `epf` float unsigned DEFAULT '0' COMMENT '(unit g P eq) Eutrophication of freshwater', + `epf` float DEFAULT '0' COMMENT '(unit g P eq) Eutrophication of freshwater', `epf_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `epm` float unsigned DEFAULT '0' COMMENT '(unit g N eq) Eutrophication of marine waters', + `epm` float DEFAULT '0' COMMENT '(unit g N eq) Eutrophication of marine waters', `epm_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', - `ept` float unsigned DEFAULT '0' COMMENT '(unit mol N eq) Terrestrial eutrophication', + `ept` float DEFAULT '0' COMMENT '(unit mol N eq) Terrestrial eutrophication', `ept_quality` int unsigned NOT NULL DEFAULT '0' COMMENT 'DataTtacking\\AbstractTracked::DATA_QUALITY_* constants', PRIMARY KEY (`id`), UNIQUE KEY `unicity` (`itemtype`, `items_id`) diff --git a/src/Impact/Type.php b/src/Impact/Type.php index 78c7bd0a..059aa5b6 100644 --- a/src/Impact/Type.php +++ b/src/Impact/Type.php @@ -148,7 +148,7 @@ public static function getImpactUnit(string $type): array **/ public static function getEmbodiedImpactLabel(string $type): string { - $embodied_impact_label = [ + $label = match ($type) { 'gwp' => __('Embodied Global warming potential', 'carbon'), 'adp' => __('Embodied Abiotic depletion potential', 'carbon'), 'pe' => __('Embodied Primary energy consumed', 'carbon'), @@ -171,8 +171,9 @@ public static function getEmbodiedImpactLabel(string $type): string 'epf' => __('Embodied Eutrophication of freshwater', 'carbon'), 'epm' => __('Embodied Eutrophication of marine waters', 'carbon'), 'ept' => __('Embodied Terrestrial eutrophication', 'carbon'), - ]; - return $embodied_impact_label[$type] ?? ''; + default => '', + }; + return $label; } /** @@ -183,7 +184,7 @@ public static function getEmbodiedImpactLabel(string $type): string **/ public static function getUsageImpactLabel(string $type): string { - static $usage_impact_label = [ + $label = match ($type) { 'gwp' => __('Usage Global warming potential', 'carbon'), 'adp' => __('Usage Abiotic depletion potential', 'carbon'), 'pe' => __('Usage Primary energy consumed', 'carbon'), @@ -206,7 +207,8 @@ public static function getUsageImpactLabel(string $type): string 'epf' => __('Usage Eutrophication of freshwater', 'carbon'), 'epm' => __('Usage Eutrophication of marine waters', 'carbon'), 'ept' => __('Usage Terrestrial eutrophication', 'carbon'), - ]; - return $usage_impact_label[$type] ?? ''; + default => '' + }; + return $label; } } From e53b243b8c2d89f7c109b1cce80c77322340d23a Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 10:17:33 +0100 Subject: [PATCH 05/11] style: code style --- src/Toolbox.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Toolbox.php b/src/Toolbox.php index 5dfc8b64..eb944174 100644 --- a/src/Toolbox.php +++ b/src/Toolbox.php @@ -201,9 +201,9 @@ public static function getWeight(float $weight): string { //TRANS: list of unit (o for octet) $units = [ - __('g', 'carbon'), + __('g', 'carbon'), __('Kg', 'carbon'), - __('t', 'carbon'), + __('t', 'carbon'), __('Kt', 'carbon'), __('Mt', 'carbon'), __('Gt', 'carbon'), @@ -320,7 +320,7 @@ public static function dynamicRound(float $number): float * @return string */ public static function getHumanReadableValue(float $value, array $unit): string - { + { switch ($unit[0]) { case 'g': return self::getWeight($value) . $unit[1]; From dd0c6073c94b5c278940fe186c62baab6135ff48 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 10:24:53 +0100 Subject: [PATCH 06/11] fix(Install): method always return true but may throw an exception on error --- install/Install.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/install/Install.php b/install/Install.php index 34c1cebb..1ba40627 100644 --- a/install/Install.php +++ b/install/Install.php @@ -105,11 +105,18 @@ public function install(array $args = []): bool global $DB; $dbFile = plugin_carbon_getSchemaPath(); - if ($dbFile === null || !$DB->runFile($dbFile)) { + if ($dbFile === null) { $this->migration->addWarningMessage("Error creating tables : " . $DB->error()); return false; } + try { + $DB->runFile($dbFile); + } catch (\RuntimeException $e) { + $this->migration->addWarningMessage("Error creating tables : " . $e->getMessage()); + return false; + } + // Execute all install sub tasks $install_dir = __DIR__ . '/install/'; $update_scripts = scandir($install_dir); From c2a7925d4e69c082dbbed37a30916a7ac0ba39ae Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 10:31:58 +0100 Subject: [PATCH 07/11] fix(Install): follow stricter lint checks --- install/install/init_datasources.php | 40 ++++++++++----------- install/migration/update_1.0.0_to_1.0.1.php | 8 ++++- install/migration/update_1.0.1_to_1.1.0.php | 9 ++++- install/migration/update_1.1.1_to_1.2.0.php | 9 ++++- install/migration/update_x.x.x_to_y.y.y.php | 9 ++++- src/Dashboard/Grid.php | 3 -- src/Impact/Embodied/Engine.php | 2 ++ src/Toolbox.php | 4 --- 8 files changed, 53 insertions(+), 31 deletions(-) diff --git a/install/install/init_datasources.php b/install/install/init_datasources.php index b7e18c64..dd4bf17a 100644 --- a/install/install/init_datasources.php +++ b/install/install/init_datasources.php @@ -104,16 +104,16 @@ Install::linkSourceZone($source_id, $zone_id, $code); // Insert into the database - $success = $DB->updateOrInsert($table, [ - 'intensity' => $intensity, - 'data_quality' => 2 // constant GlpiPlugin\Carbon\DataTracking::DATA_QUALITY_ESTIMATED - ], [ - 'date' => "$year-01-01 00:00:00", - 'plugin_carbon_sources_id' => $source_id, - 'plugin_carbon_zones_id' => $zone_id, - ]); - - if ($success === false) { + try { + $DB->updateOrInsert($table, [ + 'intensity' => $intensity, + 'data_quality' => 2 // constant GlpiPlugin\Carbon\DataTracking::DATA_QUALITY_ESTIMATED + ], [ + 'date' => "$year-01-01 00:00:00", + 'plugin_carbon_sources_id' => $source_id, + 'plugin_carbon_zones_id' => $zone_id, + ]); + } catch (\RuntimeException $e) { $file = null; // close the file throw new \RuntimeException("Failed to insert data for year $year"); } @@ -129,16 +129,16 @@ $quebec_carbon_intensity = include(dirname(__DIR__) . '/data/carbon_intensity/quebec.php'); foreach ($quebec_carbon_intensity as $year => $intensity) { - $success = $DB->updateOrInsert($table, [ - 'intensity' => $intensity, - 'data_quality' => 2 // constant GlpiPlugin\Carbon\DataTracking::DATA_QUALITY_ESTIMATED - ], [ - 'date' => "$year-01-01 00:00:00", - 'plugin_carbon_sources_id' => $source_id, - 'plugin_carbon_zones_id' => $zone_id_quebec, - ]); - - if ($success === false) { + try { + $DB->updateOrInsert($table, [ + 'intensity' => $intensity, + 'data_quality' => 2 // constant GlpiPlugin\Carbon\DataTracking::DATA_QUALITY_ESTIMATED + ], [ + 'date' => "$year-01-01 00:00:00", + 'plugin_carbon_sources_id' => $source_id, + 'plugin_carbon_zones_id' => $zone_id_quebec, + ]); + } catch (\RuntimeException $e) { $file = null; // close the file throw new \RuntimeException("Failed to insert data for year $year"); } diff --git a/install/migration/update_1.0.0_to_1.0.1.php b/install/migration/update_1.0.0_to_1.0.1.php index 975bf076..9d2ff61a 100644 --- a/install/migration/update_1.0.0_to_1.0.1.php +++ b/install/migration/update_1.0.0_to_1.0.1.php @@ -57,10 +57,16 @@ function update100to101(Migration $migration) } $dbFile = plugin_carbon_getSchemaPath($to_version); - if ($dbFile === null || !$DB->runFile($dbFile)) { + if ($dbFile === null) { $migration->addWarningMessage("Error creating tables : " . $DB->error()); $updateresult = false; } + try { + $DB->runFile($dbFile); + } catch (\RuntimeException $e) { + $migration->addWarningMessage("Error creating tables : " . $e->getMessage()); + $updateresult = false; + } // ************ Keep it at the end ************** $migration->executeMigration(); diff --git a/install/migration/update_1.0.1_to_1.1.0.php b/install/migration/update_1.0.1_to_1.1.0.php index c32d549a..6998fce6 100644 --- a/install/migration/update_1.0.1_to_1.1.0.php +++ b/install/migration/update_1.0.1_to_1.1.0.php @@ -57,10 +57,17 @@ function update101to110(Migration $migration) } $dbFile = plugin_carbon_getSchemaPath($to_version); - if ($dbFile === null || !$DB->runFile($dbFile)) { + if ($dbFile === null) { $migration->addWarningMessage("Error creating tables : " . $DB->error()); $updateresult = false; } + try { + $DB->runFile($dbFile); + } catch (\RuntimeException $e) { + $migration->addWarningMessage("Error creating tables : " . $e->getMessage()); + $updateresult = false; + } + // ************ Keep it at the end ************** $migration->executeMigration(); diff --git a/install/migration/update_1.1.1_to_1.2.0.php b/install/migration/update_1.1.1_to_1.2.0.php index 4e62e6d3..d5ca343a 100644 --- a/install/migration/update_1.1.1_to_1.2.0.php +++ b/install/migration/update_1.1.1_to_1.2.0.php @@ -57,10 +57,17 @@ function update111to120(Migration $migration) } $dbFile = plugin_carbon_getSchemaPath($to_version); - if ($dbFile === null || !$DB->runFile($dbFile)) { + if ($dbFile === null) { $migration->addWarningMessage("Error creating tables : " . $DB->error()); $updateresult = false; } + try { + $DB->runFile($dbFile); + } catch (\RuntimeException $e) { + $migration->addWarningMessage("Error creating tables : " . $e->getMessage()); + $updateresult = false; + } + // ************ Keep it at the end ************** $migration->executeMigration(); diff --git a/install/migration/update_x.x.x_to_y.y.y.php b/install/migration/update_x.x.x_to_y.y.y.php index cb9599dc..2b416877 100644 --- a/install/migration/update_x.x.x_to_y.y.y.php +++ b/install/migration/update_x.x.x_to_y.y.y.php @@ -57,10 +57,17 @@ function update001to100(Migration $migration) } $dbFile = plugin_carbon_getSchemaPath($to_version); - if ($dbFile === null || !$DB->runFile($dbFile)) { + if ($dbFile === null) { $migration->addWarningMessage("Error creating tables : " . $DB->error()); $updateresult = false; } + try { + $DB->runFile($dbFile); + } catch (\RuntimeException $e) { + $migration->addWarningMessage("Error creating tables : " . $e->getMessage()); + $updateresult = false; + } + // ************ Keep it at the end ************** $migration->executeMigration(); diff --git a/src/Dashboard/Grid.php b/src/Dashboard/Grid.php index 879990a6..e005fb5c 100644 --- a/src/Dashboard/Grid.php +++ b/src/Dashboard/Grid.php @@ -352,13 +352,10 @@ private static function getWidgetForImpact(bool $embodied, string $type): string switch (($embodied ? 'embodied' : 'usage') . ' ' . $type) { case 'embodied gwp': return 'embodied_global_warming'; - break; case 'embodied adp': return 'embodied_abiotic_depletion'; - break; case 'embodied pe': return 'embodied_primary_energy'; - break; } return ''; diff --git a/src/Impact/Embodied/Engine.php b/src/Impact/Embodied/Engine.php index c033983f..a66b2b70 100644 --- a/src/Impact/Embodied/Engine.php +++ b/src/Impact/Embodied/Engine.php @@ -34,6 +34,7 @@ use CommonGLPI; use CommonDBTM; +use DBmysql; use GlpiPlugin\Carbon\AbstractModel; use GlpiPlugin\Carbon\Config; use GlpiPlugin\Carbon\DataSource\Lca\Boaviztapi\Client; @@ -131,6 +132,7 @@ protected static function configureEngine(EmbodiedImpactInterface $engine): Embo */ private static function hasModelData(CommonDBTM $item): bool { + /** @var DBmysql $DB */ global $DB; $itemtype = get_class($item); diff --git a/src/Toolbox.php b/src/Toolbox.php index eb944174..67517d2d 100644 --- a/src/Toolbox.php +++ b/src/Toolbox.php @@ -324,18 +324,14 @@ public static function getHumanReadableValue(float $value, array $unit): string switch ($unit[0]) { case 'g': return self::getWeight($value) . $unit[1]; - break; case 'J': // To be converted into watt.hour return self::getEnergy($value / 3600) . $unit[1]; - break; case 'Wh': return self::getEnergy($value) . $unit[1]; - break; case 'm³': // Value is in m^3 return sprintf(__('%1$s %2$s', 'carbon'), $value * 1000, 'L'); - break; case 'mol': break; } From afae034a942e2fafea1d7e4db2a2eec2a8393816 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 10:43:26 +0100 Subject: [PATCH 08/11] feat(EmbodiedImpact): add search options --- src/EmbodiedImpact.php | 43 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/EmbodiedImpact.php b/src/EmbodiedImpact.php index 190ece8f..9cb30c91 100644 --- a/src/EmbodiedImpact.php +++ b/src/EmbodiedImpact.php @@ -36,6 +36,7 @@ use DBmysql; use DBmysqlIterator; use Glpi\DBAL\QuerySubQuery; +use GlpiPlugin\Carbon\Impact\Type; /** * Embodied impact of assets @@ -83,35 +84,19 @@ public function rawSearchOptions() 'datatype' => 'itemtypename', ]; - $tab[] = [ - 'id' => '5', - 'table' => $this->getTable(), - 'field' => 'gwp', - 'name' => __('Global Warming Potential', 'carbon'), - 'massiveaction' => false, - 'datatype' => 'number', - 'unit' => 'g CO2 eq', - ]; - - $tab[] = [ - 'id' => '6', - 'table' => $this->getTable(), - 'field' => 'adp', - 'name' => __('Abiotic Depletion Potential', 'carbon'), - 'massiveaction' => false, - 'datatype' => 'number', - 'unit' => 'g Sb eq', - ]; - - $tab[] = [ - 'id' => '7', - 'table' => $this->getTable(), - 'field' => 'pe', - 'name' => __('Primary energy', 'carbon'), - 'massiveaction' => false, - 'datatype' => 'number', - 'unit' => 'J', - ]; + $id = 5; + foreach (Type::getImpactTypes() as $type => $unit) { + $tab[] = [ + 'id' => $id, + 'table' => $this->getTable(), + 'field' => $type, + 'name' => Type::getEmbodiedImpactLabel($type), + 'massiveaction' => false, + 'datatype' => 'number', + 'unit' => implode(' ', $unit), + ]; + $id++; + } $tab[] = [ 'id' => SearchOptions::CARBON_EMISSION_CALC_DATE, From 860c6c3e1550a7c8fd4eb05b186e4b7dd85c1d81 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 10:44:19 +0100 Subject: [PATCH 09/11] fix(Impact\Type): better unit notation --- src/Impact/Type.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Impact/Type.php b/src/Impact/Type.php index 059aa5b6..2a7b3a30 100644 --- a/src/Impact/Type.php +++ b/src/Impact/Type.php @@ -89,12 +89,12 @@ class Type * @var array */ private static array $impact_units = [ - 'gwp' => ['g', 'CO2 eq'], + 'gwp' => ['g', 'CO₂ eq'], 'adp' => ['g', 'SB eq'], 'pe' => ['J', ''], - 'gwppb' => ['g', 'CO2 eq'], - 'gwppf' => ['g', 'CO2 eq'], - 'gwpplu' => ['g', 'CO2 eq'], + 'gwppb' => ['g', 'CO₂ eq'], + 'gwppf' => ['g', 'CO₂ eq'], + 'gwpplu' => ['g', 'CO₂ eq'], 'ir' => ['g', 'U235 eq'], 'lu' => null, 'odp' => ['g', 'CFC-11 eq'], From c22ae21d1c9392afd6116520d4b33b6b9ac8ffa3 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 11:29:04 +0100 Subject: [PATCH 10/11] fix: code lint --- install/install/init_datasources.php | 4 ++-- src/Impact/Type.php | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/install/install/init_datasources.php b/install/install/init_datasources.php index dd4bf17a..cff83fc3 100644 --- a/install/install/init_datasources.php +++ b/install/install/init_datasources.php @@ -115,7 +115,7 @@ ]); } catch (\RuntimeException $e) { $file = null; // close the file - throw new \RuntimeException("Failed to insert data for year $year"); + throw new \RuntimeException("Failed to insert data for year $year; reason: " . $e->getMessage(), $e->getCode(), $e); } } if ($progress_bar) { @@ -140,6 +140,6 @@ ]); } catch (\RuntimeException $e) { $file = null; // close the file - throw new \RuntimeException("Failed to insert data for year $year"); + throw new \RuntimeException("Failed to insert data for year $year; reason: " . $e->getMessage(), $e->getCode(), $e); } } diff --git a/src/Impact/Type.php b/src/Impact/Type.php index 2a7b3a30..9802b39f 100644 --- a/src/Impact/Type.php +++ b/src/Impact/Type.php @@ -113,6 +113,11 @@ class Type 'ept' => ['mol', 'N eq'], ]; + /** + * Undocumented function + * + * @return array + */ public static function getImpactTypes(): array { return self::$impact_types; From 6c76cf8908a19e5464033306c9e6131917d25ede Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Mon, 16 Feb 2026 11:44:11 +0100 Subject: [PATCH 11/11] fix(Type): search option construction --- src/EmbodiedImpact.php | 4 ++-- src/Impact/Type.php | 10 +++++----- tests/integration/SearchOptionTest.php | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/EmbodiedImpact.php b/src/EmbodiedImpact.php index 9cb30c91..968e3530 100644 --- a/src/EmbodiedImpact.php +++ b/src/EmbodiedImpact.php @@ -85,7 +85,7 @@ public function rawSearchOptions() ]; $id = 5; - foreach (Type::getImpactTypes() as $type => $unit) { + foreach (Type::getImpactTypes() as $type) { $tab[] = [ 'id' => $id, 'table' => $this->getTable(), @@ -93,7 +93,7 @@ public function rawSearchOptions() 'name' => Type::getEmbodiedImpactLabel($type), 'massiveaction' => false, 'datatype' => 'number', - 'unit' => implode(' ', $unit), + 'unit' => implode(' ', Type::getImpactUnit($type)), ]; $id++; } diff --git a/src/Impact/Type.php b/src/Impact/Type.php index 9802b39f..b127512d 100644 --- a/src/Impact/Type.php +++ b/src/Impact/Type.php @@ -35,8 +35,8 @@ class Type { - const IMPACT_GWP = 0; // Global warming potential - const IMPACT_ADP = 1; // Abiotic Depletion Potential + const IMPACT_GWP = 1; // Global warming potential + const IMPACT_ADP = 2; // Abiotic Depletion Potential const IMPACT_PE = 3; // Primary Energy const IMPACT_GWPPB = 4; const IMPACT_GWPPF = 5; @@ -114,9 +114,9 @@ class Type ]; /** - * Undocumented function + * get an array of impact types * - * @return array + * @return array */ public static function getImpactTypes(): array { @@ -138,7 +138,7 @@ public static function getImpactId(string $type) * Get the unit of an impact type * * @param string $type impact type name - * @return array + * @return array **/ public static function getImpactUnit(string $type): array { diff --git a/tests/integration/SearchOptionTest.php b/tests/integration/SearchOptionTest.php index e8e979b9..6a45a523 100644 --- a/tests/integration/SearchOptionTest.php +++ b/tests/integration/SearchOptionTest.php @@ -81,6 +81,25 @@ class SearchOptionTest extends CommonTestCase 'gwp_quality', 'adp_quality', 'pe_quality', + 'gwppb_quality', + 'gwppf_quality', + 'gwpplu_quality', + 'ir_quality', + 'lu_quality', + 'odp_quality', + 'pm_quality', + 'pocp_quality', + 'wu_quality', + 'mips_quality', + 'adpe_quality', + 'adpf_quality', + 'ap_quality', + 'ctue_quality', + 'ctuh_c_quality', + 'ctuh_nc_quality', + 'epf_quality', + 'epm_quality', + 'ept_quality', ], Location::class => [], Zone::class => [