├── doc ├── MONITOR.md ├── PROVIDER.md ├── POLICY.md ├── INSTALL.md ├── USAGE.md └── CONFIG.md ├── img └── ecofreq_tldr.png ├── ecofreq ├── __init__.py ├── installer │ ├── ecofreq.service │ └── install.sh ├── config.py ├── helpers │ ├── __init__.py │ ├── ipmi.py │ ├── geo.py │ ├── suspend.py │ ├── docker.py │ ├── nvidia.py │ ├── cgroup.py │ ├── amd.py │ └── cpu.py ├── monitors │ ├── common.py │ ├── freq.py │ ├── idle.py │ ├── manager.py │ └── energy.py ├── config │ ├── docker.cfg │ ├── mock.cfg │ ├── ukgrid.cfg │ ├── default.cfg │ ├── watttime.cfg │ ├── electricitymaps.cfg │ ├── co2signal.cfg │ ├── mqtt.cfg │ ├── stromgedacht.cfg │ ├── energycharts.cfg │ └── README.md ├── install.py ├── providers │ ├── mqtt.py │ ├── manager.py │ └── common.py ├── policy │ ├── common.py │ ├── idle.py │ ├── manager.py │ ├── gpu.py │ ├── governor.py │ └── cpu.py ├── utils.py ├── ipc.py ├── mqtt.py ├── ecorun.py ├── ecoctl.py ├── ecostat.py ├── data │ └── cpu_tdp.csv └── ecofreq.py ├── ecofreq.service ├── config ├── docker.cfg ├── mock.cfg ├── ecofreq.cfg.ukgrid ├── ecofreq.cfg.watttime ├── ecofreq.cfg.electricitymaps ├── ecofreq.cfg.co2signal ├── mqtt.cfg ├── ecofreq.cfg.stromgedacht ├── ecofreq.cfg.energycharts └── README.md ├── pyproject.toml ├── ecofreq.cfg.templ ├── .gitignore ├── README.md └── LICENSE.txt /doc/MONITOR.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/ecofreq_tldr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amkozlov/eco-freq/HEAD/img/ecofreq_tldr.png -------------------------------------------------------------------------------- /ecofreq/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | try: 4 | __version__ = importlib.metadata.distribution(__name__).version 5 | except Exception: 6 | __version__ = "unknown" -------------------------------------------------------------------------------- /ecofreq.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Adjust CPU frequency according to CO2-intensity of the energy mix 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=/ecofreq.py 7 | 8 | [Install] 9 | WantedBy=multi-user.target 10 | 11 | -------------------------------------------------------------------------------- /ecofreq/installer/ecofreq.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Adjust CPU frequency according to CO2-intensity of the energy mix 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart= 7 | User= 8 | Group= 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | 13 | -------------------------------------------------------------------------------- /ecofreq/config.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | JOULES_IN_KWH = 3.6e6 4 | OPTION_DISABLED = ["none", "off"] 5 | 6 | TS_FORMAT = "%Y-%m-%dT%H:%M:%S" 7 | 8 | HOMEDIR = pathlib.Path(__file__).parent 9 | DATADIR = HOMEDIR / "data" 10 | CONFIGDIR = HOMEDIR / "config" 11 | SCRIPTDIR = HOMEDIR / "scripts" 12 | LOG_FILE = "/var/log/ecofreq.log" 13 | SHM_FILE = "/dev/shm/ecofreq" 14 | -------------------------------------------------------------------------------- /ecofreq/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from ecofreq.helpers.cpu import CpuInfoHelper, CpuFreqHelper, CpuPowerHelper, LinuxPowercapHelper 2 | from ecofreq.helpers.amd import AMDEsmiHelper, AMDRaplMsrHelper 3 | from ecofreq.helpers.nvidia import NvidiaGPUHelper 4 | from ecofreq.helpers.cgroup import LinuxCgroupHelper, LinuxCgroupV1Helper, LinuxCgroupV2Helper 5 | from ecofreq.helpers.suspend import SuspendHelper 6 | from ecofreq.helpers.ipmi import IPMIHelper 7 | from ecofreq.helpers.docker import DockerHelper 8 | from ecofreq.helpers.geo import GeoHelper 9 | 10 | __all__ = [ "cpu", "cgroup" ] -------------------------------------------------------------------------------- /ecofreq/monitors/common.py: -------------------------------------------------------------------------------- 1 | class Monitor(object): 2 | def __init__(self, config): 3 | self.interval = int(config["monitor"]["interval"]) 4 | self.period_samples = 0 5 | self.total_samples = 0 6 | 7 | def reset_period(self): 8 | self.period_samples = 0 9 | 10 | # subclasses must override this to call actual update routine 11 | def update_impl(self): 12 | pass 13 | 14 | def update(self): 15 | self.update_impl() 16 | self.period_samples += 1 17 | self.total_samples += 1 18 | 19 | # subclasses must override this 20 | def get_stats(self): 21 | return {} 22 | -------------------------------------------------------------------------------- /config/docker.cfg: -------------------------------------------------------------------------------- 1 | [provider] 2 | all=mock 3 | Interval=10 4 | 5 | [mock] 6 | CO2Range=50-300 7 | #CO2File=data/co2trace.tsv 8 | 9 | [cpu_policy] 10 | Control=docker 11 | # Comma-separated list of container names/IDs for which power scaling should be applied (default: all) 12 | #Containers=my_cool_container,e8f7089b12f1 13 | Metric=co2 14 | DefaultGovernor=step:20=1.0:100=0.7:200=0.5 15 | Governor=default 16 | 17 | [gpu_policy] 18 | Control=auto 19 | DefaultGovernor=step:20=1.0:100=0.7:200=0.5 20 | Governor=default 21 | 22 | [monitor] 23 | PowerSensor=auto 24 | Interval=5 25 | 26 | #[idle] 27 | #IdleMonitor=off 28 | -------------------------------------------------------------------------------- /ecofreq/config/docker.cfg: -------------------------------------------------------------------------------- 1 | [provider] 2 | all=mock 3 | Interval=10 4 | 5 | [mock] 6 | CO2Range=50-300 7 | #CO2File=data/co2trace.tsv 8 | 9 | [cpu_policy] 10 | Control=docker 11 | # Comma-separated list of container names/IDs for which power scaling should be applied (default: all) 12 | #Containers=my_cool_container,e8f7089b12f1 13 | Metric=co2 14 | DefaultGovernor=step:20=1.0:100=0.7:200=0.5 15 | Governor=default 16 | 17 | [gpu_policy] 18 | Control=auto 19 | DefaultGovernor=step:20=1.0:100=0.7:200=0.5 20 | Governor=default 21 | 22 | [monitor] 23 | PowerSensor=auto 24 | Interval=5 25 | 26 | #[idle] 27 | #IdleMonitor=off 28 | -------------------------------------------------------------------------------- /config/mock.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | #LogCO2Extra=true 3 | #LogCost=true 4 | 5 | [provider] 6 | all=mock 7 | Interval=10 8 | 9 | [mock] 10 | CO2Range=50-300 11 | #CO2File=data/co2trace.tsv 12 | 13 | [policy] 14 | #Control=Power 15 | #Control=Frequency 16 | #Control=cgroup 17 | Metric=co2 18 | #Metric=index 19 | DefaultGovernor=step:20=1.0:100=0.7:200=0.5 20 | #DefaultGovernor=list:very low=max:low=0.9:moderate=0.8:high=0.7:very high=0.6 21 | Governor=default 22 | 23 | [monitor] 24 | PowerSensor=auto 25 | Interval=5 26 | 27 | [idle] 28 | IdleMonitor=on 29 | LoadCutoff=0.10 30 | # 1 = 1min average, 2 = 5min, 3 = 15min 31 | LoadPeriod=1 32 | # Switch to low-energy standby mode (suspend) after x seconds of idling 33 | #SuspendAfter=60 34 | -------------------------------------------------------------------------------- /ecofreq/config/mock.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | #LogCO2Extra=true 3 | #LogCost=true 4 | 5 | [provider] 6 | all=mock 7 | Interval=10 8 | 9 | [mock] 10 | #CO2Range=50-300 11 | CO2File=data/co2trace.tsv 12 | 13 | [policy] 14 | #Control=Power 15 | #Control=Frequency 16 | #Control=cgroup 17 | Metric=co2 18 | #Metric=index 19 | DefaultGovernor=step:10=1.0:200=0.7:400=0.5 20 | #DefaultGovernor=list:very low=max:low=0.9:moderate=0.8:high=0.7:very high=0.6 21 | Governor=default 22 | 23 | [monitor] 24 | PowerSensor=auto 25 | Interval=5 26 | 27 | [idle] 28 | IdleMonitor=on 29 | LoadCutoff=0.10 30 | # 1 = 1min average, 2 = 5min, 3 = 15min 31 | LoadPeriod=1 32 | # Switch to low-energy standby mode (suspend) after x seconds of idling 33 | #SuspendAfter=60 34 | -------------------------------------------------------------------------------- /ecofreq/install.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | 4 | from ecofreq.config import HOMEDIR 5 | 6 | class EcofreqInstaller(object): 7 | INSTALL_SH = HOMEDIR / "installer" / "install.sh" 8 | 9 | @classmethod 10 | def install(cls, args): 11 | print("Installing EcoFreq service...") 12 | efscript = sys.argv[0] 13 | cmd = [str(cls.INSTALL_SH), "-e", efscript] 14 | if args.duser: 15 | cmd += ["-u", args.duser] 16 | if args.dgroup: 17 | cmd += ["-g", args.dgroup] 18 | if args.cfg_file: 19 | cmd += ["-c", args.cfg_file] 20 | # print(cmd) 21 | subprocess.run(cmd) 22 | 23 | @classmethod 24 | def uninstall(cls, args): 25 | print("Removing EcoFreq service...") 26 | cmd = [str(cls.INSTALL_SH), "-U"] 27 | subprocess.run(cmd) 28 | -------------------------------------------------------------------------------- /config/ecofreq.cfg.ukgrid: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Using UK National Grid API for real-time carbon intensity: https://carbonintensity.org.uk/ 8 | all=ukgrid 9 | # Constant electricity price in p/kWh 10 | price=const:30 11 | # Optional: use dynamic pricing 12 | #price=octopus 13 | 14 | [ukgrid] 15 | PostCode=CB10 16 | # List of region codes: https://carbon-intensity.github.io/api-definitions/#region-list 17 | #RegionID=10 18 | 19 | [octopus] 20 | Product=AGILE-18-02-21 21 | Tariff=E-1R-AGILE-18-02-21-C 22 | UseCache=F 23 | 24 | [policy] 25 | Control=auto 26 | #DefaultGovernor=maxperf 27 | Metric=index 28 | DefaultGovernor=list:very low=max:low=max:moderate=0.8:high=0.6:very high=0.6 29 | Governor=default 30 | 31 | [monitor] 32 | PowerSensor=auto 33 | Interval=5 34 | -------------------------------------------------------------------------------- /ecofreq/config/ukgrid.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Using UK National Grid API for real-time carbon intensity: https://carbonintensity.org.uk/ 8 | all=ukgrid 9 | # Constant electricity price in p/kWh 10 | price=const:30 11 | # Optional: use dynamic pricing 12 | #price=octopus 13 | 14 | [ukgrid] 15 | PostCode=CB10 16 | # List of region codes: https://carbon-intensity.github.io/api-definitions/#region-list 17 | #RegionID=10 18 | 19 | [octopus] 20 | Product=AGILE-18-02-21 21 | Tariff=E-1R-AGILE-18-02-21-C 22 | UseCache=F 23 | 24 | [policy] 25 | Control=auto 26 | #DefaultGovernor=maxperf 27 | Metric=index 28 | DefaultGovernor=list:very low=max:low=max:moderate=0.8:high=0.6:very high=0.6 29 | Governor=default 30 | 31 | [monitor] 32 | PowerSensor=auto 33 | Interval=5 34 | -------------------------------------------------------------------------------- /ecofreq/helpers/ipmi.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output,DEVNULL,CalledProcessError 2 | 3 | class IPMIHelper(object): 4 | @classmethod 5 | def available(cls): 6 | return cls.get_power() is not None 7 | 8 | @classmethod 9 | def info(cls): 10 | print("IPMI available: ", end ="") 11 | if cls.available(): 12 | print("YES") 13 | else: 14 | print("NO") 15 | 16 | @classmethod 17 | def get_power(cls): 18 | try: 19 | out = check_output("ipmitool dcmi power reading", shell=True, stderr=DEVNULL, universal_newlines=True) 20 | for line in out.split("\n"): 21 | tok = [x.strip() for x in line.split(":")] 22 | if tok[0] == "Instantaneous power reading": 23 | pwr = tok[1].split()[0] 24 | return float(pwr) 25 | return None 26 | except CalledProcessError: 27 | return None 28 | -------------------------------------------------------------------------------- /ecofreq/helpers/geo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import urllib.request 4 | 5 | class GeoHelper(object): 6 | API_URL = "http://ipinfo.io" 7 | 8 | @classmethod 9 | def get_my_geoinfo(self): 10 | req = urllib.request.Request(self.API_URL) 11 | # req.add_header("User-Agent", "Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11") 12 | 13 | try: 14 | resp = urllib.request.urlopen(req).read() 15 | js = json.loads(resp) 16 | return js 17 | except: 18 | e = sys.exc_info()[0] 19 | print ("Exception: ", e) 20 | return None 21 | 22 | @classmethod 23 | def get_my_coords(self): 24 | try: 25 | js = self.get_my_geoinfo() 26 | lat, lon = js['loc'].split(",") 27 | except: 28 | e = sys.exc_info()[0] 29 | print ("Exception: ", e) 30 | lat, lon = None, None 31 | return lat, lon 32 | -------------------------------------------------------------------------------- /ecofreq/providers/mqtt.py: -------------------------------------------------------------------------------- 1 | from ecofreq.providers.common import EcoProvider 2 | from ecofreq.mqtt import MQTTManager 3 | 4 | class MQTTEcoProvider(EcoProvider): 5 | LABEL="mqtt" 6 | 7 | def __init__(self, config, glob_interval, label): 8 | EcoProvider.__init__(self, config, glob_interval) 9 | self.label = label 10 | self.set_config(config) 11 | self.mqtt_client = MQTTManager.add_client(self.label, config) 12 | 13 | def set_config(self, config): 14 | self.topic = config.get("topic", None) 15 | 16 | def get_config(self): 17 | cfg = super().get_config() 18 | cfg["topic"] = self.topic 19 | cfg["label"] = self.label 20 | return cfg 21 | 22 | def get_data(self): 23 | data = {} 24 | val = self.mqtt_client.get_msg() 25 | # print(val) 26 | data[self.FIELD_DEFAULT] = float(val) if val is not None else None 27 | # print(data) 28 | return data 29 | -------------------------------------------------------------------------------- /ecofreq/policy/common.py: -------------------------------------------------------------------------------- 1 | from ecofreq.policy.governor import Governor 2 | 3 | class EcoPolicy(object): 4 | UNIT={} 5 | def __init__(self, config): 6 | self.debug = False 7 | 8 | def info_string(self): 9 | g = self.governor.info_string(self.UNIT) if self.governor else "None" 10 | return type(self).__name__ + " (governor = " + g + ")" 11 | 12 | def init_governor(self, config, vmin, vmax, vround=None): 13 | self.governor = Governor.from_config(config, vmin, vmax, self.UNIT) 14 | if self.governor and vround: 15 | self.governor.val_round = vround 16 | 17 | def get_config(self, config={}): 18 | config["control"] = type(self).__name__ 19 | config["governor"] = self.governor.info_string(self.UNIT) if self.governor else "none" 20 | return config 21 | 22 | def co2val(self, co2): 23 | if self.governor: 24 | return self.governor.co2val(co2) 25 | else: 26 | return None 27 | -------------------------------------------------------------------------------- /doc/PROVIDER.md: -------------------------------------------------------------------------------- 1 | # List of supported carbon/price providers 2 | 3 | See also: https://github.com/amkozlov/eco-freq/blob/main/config/README.md 4 | 5 | ## EnergyCharts 6 | Website: https://energy-charts.info/ 7 | Metrics: traffic light, %RE, wholesale price 8 | 9 | ## ElectricityMaps 10 | Website: https://static.electricitymaps.com/api/docs/index.html 11 | Metrics: gCO2/kWh, %RE 12 | 13 | ## WattTime 14 | Website: https://www.watttime.org/ 15 | Metrics: gCO2/kWh 16 | 17 | ## ESO National Grid (UK) 18 | Website: https://carbonintensity.org.uk/ 19 | Metrics: gCO2/kWh, traffic light 20 | 21 | ## GridStatus.io 22 | Website: https://api.gridstatus.io/docs 23 | Metrics: wholesale price 24 | 25 | ## StromGedacht 26 | Website: https://www.stromgedacht.de/ 27 | Metrics: traffic light 28 | 29 | ## Tibber 30 | Website: https://tibber.com/ 31 | Metrics: retail price 32 | 33 | ## Octopus 34 | Website: https://octopus.energy/ 35 | Metrics: retail price 36 | 37 | ## Awattar 38 | Website: https://www.awattar.at/ 39 | Metrics: retail price 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ecofreq/config/default.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use EnergyChart electricity traffic light: https://api.energy-charts.info/ 8 | all=energycharts 9 | # CO2 Option1: use ElectricityMaps for carbon accounting 10 | #co2=electricitymaps 11 | # CO2 Option2: use constant carbon intensity (gCO2e/kwh) 12 | #co2=const:350 13 | 14 | [energycharts] 15 | #List of available countries/price zones: https://api.energy-charts.info/#/ 16 | Country=DE 17 | PostCode=69118 18 | PriceZone=DE-LU 19 | 20 | [electricitymaps] 21 | Zone=DE 22 | #Token= 23 | DisableEstimations=False 24 | EmissionFactorType=lifecycle 25 | 26 | [policy] 27 | Control=auto 28 | # No dynamic power scaling by default: 29 | #DefaultGovernor=maxperf 30 | # Sample powercap policy: black(grid congestion) or red(low RE) -> 50%, yellow(medium RE) -> 70%, green(high RE) -> 100% 31 | Metric=index 32 | DefaultGovernor=list:black=0.5:red=0.5:yellow=0.7:green=max 33 | Governor=default 34 | 35 | [monitor] 36 | PowerSensor=auto 37 | Interval=5 38 | -------------------------------------------------------------------------------- /ecofreq/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | 4 | def read_value(fname, field=0, sep=' '): 5 | with open(fname) as f: 6 | s = f.readline().rstrip("\n") 7 | if field == 0: 8 | return s 9 | else: 10 | return s.split(sep)[field] 11 | 12 | def read_int_value(fname): 13 | return int(read_value(fname)) 14 | 15 | def write_value(fname, val): 16 | if os.path.isfile(fname): 17 | with open(fname, "w") as f: 18 | f.write(str(val)) 19 | return True 20 | else: 21 | return False 22 | 23 | def safe_round(val): 24 | return round(val) if (isinstance(val, float)) else val 25 | 26 | def getbool(x): 27 | if isinstance(x, str): 28 | return True if x.lower() in ['1', 'y', 'yes', 'true', 'on'] else False 29 | else: 30 | return x 31 | 32 | class NAFormatter(string.Formatter): 33 | def __init__(self, missing='NA'): 34 | self.missing = missing 35 | 36 | def format_field(self, value, spec): 37 | if value == None: 38 | value = self.missing 39 | spec = spec.replace("f", "s") 40 | return super(NAFormatter, self).format_field(value, spec) 41 | -------------------------------------------------------------------------------- /config/ecofreq.cfg.watttime: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use WattTime carbpn API 8 | all=watttime 9 | # Price Option1: constant price in ct/kwh 10 | #price=const:30 11 | # Price Option2: Day-ahead location prices from gridstatus.io 12 | price=gridstatus.io 13 | 14 | [watttime] 15 | username=YOUR_USERNAME 16 | password=YOUR_PASSWORD 17 | region=CAISO_NORTH 18 | #UseIndex=False 19 | #UseForecast=False 20 | 21 | [gridstatus.io] 22 | # Get API token here: https://www.gridstatus.io/api 23 | Token=YOUR_TOKEN 24 | ISO=ercot 25 | Location=HB_NORTH 26 | # Available datasets: https://www.gridstatus.io/datasets 27 | #Dataset=ercot_spp_day_ahead_hourly 28 | #PriceField=spp 29 | 30 | [policy] 31 | Control=auto 32 | # No dynamic power scaling by default: 33 | #DefaultGovernor=maxperf 34 | # Use relative (percentile) carbon intesity 35 | Metric=index 36 | DefaultGovernor=step:33=0.8:66=0.6 37 | # Use absolute marginal CO2 intensity (MOER): >200 g/Kwh -> 80%, >400 g/kWh -> 60% 38 | #Metric=co2 39 | #DefaultGovernor=step:200=0.8:400=0.6 40 | Governor=default 41 | 42 | [monitor] 43 | PowerSensor=auto 44 | Interval=5 45 | -------------------------------------------------------------------------------- /ecofreq/config/watttime.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use WattTime carbpn API 8 | all=watttime 9 | # Price Option1: constant price in ct/kwh 10 | #price=const:30 11 | # Price Option2: Day-ahead location prices from gridstatus.io 12 | price=gridstatus.io 13 | 14 | [watttime] 15 | username=YOUR_USERNAME 16 | password=YOUR_PASSWORD 17 | region=CAISO_NORTH 18 | #UseIndex=False 19 | #UseForecast=False 20 | 21 | [gridstatus.io] 22 | # Get API token here: https://www.gridstatus.io/api 23 | Token=YOUR_TOKEN 24 | ISO=ercot 25 | Location=HB_NORTH 26 | # Available datasets: https://www.gridstatus.io/datasets 27 | #Dataset=ercot_spp_day_ahead_hourly 28 | #PriceField=spp 29 | 30 | [policy] 31 | Control=auto 32 | # No dynamic power scaling by default: 33 | #DefaultGovernor=maxperf 34 | # Use relative (percentile) carbon intesity 35 | Metric=index 36 | DefaultGovernor=step:33=0.8:66=0.6 37 | # Use absolute marginal CO2 intensity (MOER): >200 g/Kwh -> 80%, >400 g/kWh -> 60% 38 | #Metric=co2 39 | #DefaultGovernor=step:200=0.8:400=0.6 40 | Governor=default 41 | 42 | [monitor] 43 | PowerSensor=auto 44 | Interval=5 45 | -------------------------------------------------------------------------------- /config/ecofreq.cfg.electricitymaps: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use ElectricityMaps for carbon accounting 8 | all=electricitymaps 9 | # Price Option1 (default): wholesale day-ahead market price by EnergyCharts 10 | price=energycharts 11 | # Price Option2: constant price in ct/kwh 12 | #price=const:30 13 | # Price Option3: dynamic price (tibber, octopus or awattar) 14 | #price=tibber 15 | 16 | [electricitymaps] 17 | Zone=ES 18 | #Token= 19 | DisableEstimations=False 20 | EmissionFactorType=lifecycle 21 | 22 | [energycharts] 23 | #List of available countries/price zones: https://api.energy-charts.info/#/ 24 | Country=ES 25 | #PostCode= 26 | PriceZone=ES 27 | 28 | [tibber] 29 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 30 | UseCache=True 31 | 32 | [octopus] 33 | Product=AGILE-18-02-21 34 | Tariff=E-1R-AGILE-18-02-21-C 35 | UseCache=F 36 | 37 | [policy] 38 | Control=auto 39 | # No dynamic power scaling by default: 40 | #DefaultGovernor=maxperf 41 | # Sample powercap policy: >100 g/kWh -> 70%, >200 g/kWh -> 50% 42 | Metric=co2 43 | DefaultGovernor=step:100=0.7:200=0.5 44 | Governor=default 45 | 46 | [monitor] 47 | PowerSensor=auto 48 | Interval=5 49 | -------------------------------------------------------------------------------- /ecofreq/config/electricitymaps.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use ElectricityMaps for carbon accounting 8 | all=electricitymaps 9 | # Price Option1 (default): wholesale day-ahead market price by EnergyCharts 10 | price=energycharts 11 | # Price Option2: constant price in ct/kwh 12 | #price=const:30 13 | # Price Option3: dynamic price (tibber, octopus or awattar) 14 | #price=tibber 15 | 16 | [electricitymaps] 17 | Zone=ES 18 | #Token= 19 | DisableEstimations=False 20 | EmissionFactorType=lifecycle 21 | 22 | [energycharts] 23 | #List of available countries/price zones: https://api.energy-charts.info/#/ 24 | Country=ES 25 | #PostCode= 26 | PriceZone=ES 27 | 28 | [tibber] 29 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 30 | UseCache=True 31 | 32 | [octopus] 33 | Product=AGILE-18-02-21 34 | Tariff=E-1R-AGILE-18-02-21-C 35 | UseCache=F 36 | 37 | [policy] 38 | Control=auto 39 | # No dynamic power scaling by default: 40 | #DefaultGovernor=maxperf 41 | # Sample powercap policy: >100 g/kWh -> 70%, >200 g/kWh -> 50% 42 | Metric=co2 43 | DefaultGovernor=step:100=0.7:200=0.5 44 | Governor=default 45 | 46 | [monitor] 47 | PowerSensor=auto 48 | Interval=5 49 | -------------------------------------------------------------------------------- /config/ecofreq.cfg.co2signal: -------------------------------------------------------------------------------- 1 | [general] 2 | #logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # CO2 Option 1: Use ElectricityMaps v3 API 8 | all=electricitymaps 9 | # CO2 Option 2: Use legacy CO2Signal API (requires token) 10 | #all=co2signal 11 | # Price Option1: constant price in ct/kwh 12 | price=const:30 13 | # Price Option2: dynamic price (e.g. tibber) 14 | #price=tibber 15 | 16 | [electricitymaps] 17 | Zone=CL-SEN 18 | #Token= 19 | DisableEstimations=False 20 | EmissionFactorType=lifecycle 21 | 22 | [co2signal] 23 | #Please get your free API token here: https://co2signal.com/ 24 | Token=YOUR_TOKEN 25 | #Please look up your grid zone here: https://www.electricitymap.org 26 | # Example: Chile 27 | Country=CL-SEN 28 | 29 | [tibber] 30 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 31 | UseCache=True 32 | 33 | [octopus] 34 | Product=AGILE-18-02-21 35 | Tariff=E-1R-AGILE-18-02-21-C 36 | UseCache=F 37 | 38 | [policy] 39 | Control=auto 40 | # No dynamic power scaling by default: 41 | #DefaultGovernor=maxperf 42 | # Sample powercap policy: >100 g/kWh -> 70%, >300 g/kWh -> 50% 43 | Metric=co2 44 | DefaultGovernor=step:100=0.7:300=0.5 45 | Governor=default 46 | 47 | [monitor] 48 | PowerSensor=auto 49 | Interval=5 50 | -------------------------------------------------------------------------------- /ecofreq/config/co2signal.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | #logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # CO2 Option 1: Use ElectricityMaps v3 API 8 | all=electricitymaps 9 | # CO2 Option 2: Use legacy CO2Signal API (requires token) 10 | #all=co2signal 11 | # Price Option1: constant price in ct/kwh 12 | price=const:30 13 | # Price Option2: dynamic price (e.g. tibber) 14 | #price=tibber 15 | 16 | [electricitymaps] 17 | Zone=CL-SEN 18 | #Token= 19 | DisableEstimations=False 20 | EmissionFactorType=lifecycle 21 | 22 | [co2signal] 23 | #Please get your free API token here: https://co2signal.com/ 24 | Token=YOUR_TOKEN 25 | #Please look up your grid zone here: https://www.electricitymap.org 26 | # Example: Chile 27 | Country=CL-SEN 28 | 29 | [tibber] 30 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 31 | UseCache=True 32 | 33 | [octopus] 34 | Product=AGILE-18-02-21 35 | Tariff=E-1R-AGILE-18-02-21-C 36 | UseCache=F 37 | 38 | [policy] 39 | Control=auto 40 | # No dynamic power scaling by default: 41 | #DefaultGovernor=maxperf 42 | # Sample powercap policy: >100 g/kWh -> 70%, >300 g/kWh -> 50% 43 | Metric=co2 44 | DefaultGovernor=step:100=0.7:300=0.5 45 | Governor=default 46 | 47 | [monitor] 48 | PowerSensor=auto 49 | Interval=5 50 | -------------------------------------------------------------------------------- /ecofreq/helpers/suspend.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from ecofreq.utils import read_value,write_value 4 | 5 | class SuspendHelper(object): 6 | SYS_PWR="/sys/power/" 7 | SYS_PWR_STATE=SYS_PWR+"state" 8 | SYS_PWR_MEMSLEEP=SYS_PWR+"mem_sleep" 9 | S2MEM="mem" 10 | S2DISK="disk" 11 | S2IDLE="s2idle" 12 | S2RAM="deep" 13 | 14 | @classmethod 15 | def available(cls): 16 | return os.path.isdir(cls.SYS_PWR) 17 | 18 | @classmethod 19 | def supported_modes(cls): 20 | supported_modes = [] 21 | if cls.available(): 22 | supported_modes = read_value(cls.SYS_PWR_STATE).split(" ") 23 | if cls.S2MEM in supported_modes: 24 | supported_modes += read_value(cls.SYS_PWR_MEMSLEEP).split(" ") 25 | return supported_modes 26 | 27 | @classmethod 28 | def info(cls): 29 | print("Suspend-to-RAM available: ", end ="") 30 | def_s2ram = "[" + cls.S2RAM + "]" 31 | if def_s2ram in cls.supported_modes(): 32 | print("YES") 33 | else: 34 | print("NO") 35 | print("Suspend modes supported:", " ".join(cls.supported_modes())) 36 | 37 | @classmethod 38 | def suspend(cls, mode=S2RAM): 39 | if mode == cls.S2RAM: 40 | write_value(cls.SYS_PWR_MEMSLEEP, mode) 41 | mode = cls.S2MEM 42 | write_value(cls.SYS_PWR_STATE, mode) 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['hatchling', 'wheel'] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "ecofreq" 7 | version = "0.0.7" 8 | authors = [ 9 | { name="Oleksiy Kozlov", email="alexey.kozlov@h-its.org" }, 10 | ] 11 | description = "EcoFreq: Dynamic carbon- and price-aware power scaling for CPUs and GPUs" 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | classifiers = [ 15 | "Development Status :: 3 - Alpha", 16 | "Programming Language :: Python :: 3", 17 | "Operating System :: POSIX :: Linux", 18 | "License :: Free for non-commercial use", 19 | #CC-BY-NC-SA-4.0 20 | ] 21 | dependencies = [ 22 | "requests", 23 | "typing-extensions", 24 | "elevate" 25 | ] 26 | 27 | [project.optional-dependencies] 28 | mqtt = ["aiomqtt"] 29 | 30 | [tool.hatch.build.targets.wheel] 31 | packages = ["ecofreq"] 32 | 33 | [tool.hatch.build.targets.sdist] 34 | only-include = ["ecofreq/", "docs/", "*.md", "LICENSE.txt" ] 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/amkozlov/eco-freq" 38 | Issues = "https://github.com/amkozlov/eco-freq/issues" 39 | 40 | [project.scripts] 41 | ecofreq = "ecofreq.ecofreq:main" 42 | ecorun = "ecofreq.ecorun:main" 43 | ecoctl = "ecofreq.ecoctl:main" 44 | ecostat = "ecofreq.ecostat:main" 45 | 46 | -------------------------------------------------------------------------------- /config/mqtt.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | LogCO2Extra=true 3 | LogCost=true 4 | LogMQTT=true 5 | 6 | [provider] 7 | index=mqtt_pv_input 8 | #index=mqtt_bat_percent 9 | fossil_pct=mqtt_ac_input 10 | all=mock 11 | Interval=1 12 | 13 | [mqtt_pv_input] 14 | host=localhost 15 | topic=eb3a/dc_input_power 16 | 17 | [mqtt_ac_input] 18 | host=localhost 19 | topic=eb3a/ac_input_power 20 | 21 | [mqtt_bat_percent] 22 | host=localhost 23 | topic=eb3a/total_battery_percent 24 | 25 | [mqtt_power] 26 | Host=localhost 27 | Topic=um25c/power 28 | Interval=1 29 | 30 | [mqtt_logger] 31 | Host=localhost 32 | PubTopic=ecofreq/status 33 | PubFields=avg_power,last_co2kwh,last_price 34 | 35 | [mock] 36 | #CO2Range=50-100 37 | CO2File=data/co2trace.tsv 38 | 39 | [policy] 40 | #Control=Power 41 | #Control=Frequency 42 | #Control=cgroup 43 | #Metric=co2 44 | #Metric=index 45 | DefaultGovernor=maxperf 46 | #DefaultGovernor=step:20=1.0:100=0.7:200=0.5 47 | #DefaultGovernor=list:very low=max:low=0.9:moderate=0.8:high=0.7:very high=0.6 48 | Governor=default 49 | 50 | [monitor] 51 | #PowerSensor=auto 52 | PowerSensor=mqtt 53 | Interval=1 54 | 55 | [idle] 56 | IdleMonitor=off 57 | LoadCutoff=0.10 58 | # 1 = 1min average, 2 = 5min, 3 = 15min 59 | LoadPeriod=1 60 | # Switch to low-energy standby mode (suspend) after x seconds of idling 61 | #SuspendAfter=60 62 | -------------------------------------------------------------------------------- /ecofreq/config/mqtt.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | LogCO2Extra=true 3 | LogCost=true 4 | LogMQTT=true 5 | 6 | [provider] 7 | index=mqtt_pv_input 8 | #index=mqtt_bat_percent 9 | fossil_pct=mqtt_ac_input 10 | all=mock 11 | Interval=1 12 | 13 | [mqtt_pv_input] 14 | host=localhost 15 | topic=eb3a/dc_input_power 16 | 17 | [mqtt_ac_input] 18 | host=localhost 19 | topic=eb3a/ac_input_power 20 | 21 | [mqtt_bat_percent] 22 | host=localhost 23 | topic=eb3a/total_battery_percent 24 | 25 | [mqtt_power] 26 | Host=localhost 27 | Topic=um25c/power 28 | Interval=1 29 | 30 | [mqtt_logger] 31 | Host=localhost 32 | PubTopic=ecofreq/status 33 | PubFields=avg_power,last_co2kwh,last_price 34 | 35 | [mock] 36 | #CO2Range=50-100 37 | CO2File=data/co2trace.tsv 38 | 39 | [policy] 40 | #Control=Power 41 | #Control=Frequency 42 | #Control=cgroup 43 | #Metric=co2 44 | #Metric=index 45 | DefaultGovernor=maxperf 46 | #DefaultGovernor=step:20=1.0:100=0.7:200=0.5 47 | #DefaultGovernor=list:very low=max:low=0.9:moderate=0.8:high=0.7:very high=0.6 48 | Governor=default 49 | 50 | [monitor] 51 | #PowerSensor=auto 52 | PowerSensor=mqtt 53 | Interval=1 54 | 55 | [idle] 56 | IdleMonitor=off 57 | LoadCutoff=0.10 58 | # 1 = 1min average, 2 = 5min, 3 = 15min 59 | LoadPeriod=1 60 | # Switch to low-energy standby mode (suspend) after x seconds of idling 61 | #SuspendAfter=60 62 | -------------------------------------------------------------------------------- /ecofreq/policy/idle.py: -------------------------------------------------------------------------------- 1 | from ecofreq.monitors.idle import IdleMonitor 2 | from ecofreq.helpers.suspend import SuspendHelper 3 | 4 | class IdlePolicy(object): 5 | 6 | @classmethod 7 | def from_config(cls, config): 8 | p = None 9 | if "idle" in config: 10 | if "SuspendAfter" in config["idle"]: 11 | p = SuspendIdlePolicy(config) 12 | return p 13 | 14 | def init_monitors(self, monman): 15 | self.idlemon = monman.get_by_class(IdleMonitor) 16 | 17 | def init_logger(self, logger): 18 | self.log = logger 19 | 20 | def info_string(self): 21 | return type(self).__name__ + " (timeout = " + str(self.idle_timeout) + " sec)" 22 | 23 | def check_idle(self): 24 | if self.idlemon and self.idlemon.idle_duration > self.idle_timeout: 25 | duration = self.idlemon.idle_duration 26 | self.idlemon.reset() 27 | self.on_idle(duration) 28 | return True 29 | else: 30 | return False 31 | 32 | def on_idle(self, idle_duration): 33 | pass 34 | 35 | class SuspendIdlePolicy(IdlePolicy): 36 | def __init__(self, config): 37 | self.idle_timeout = int(config["idle"].get('SuspendAfter', 600)) 38 | self.mode = config["idle"].get('SuspendMode', SuspendHelper.S2RAM) 39 | 40 | def on_idle(self, idle_duration): 41 | if self.log: 42 | self.log.print_cmd("suspend") 43 | SuspendHelper.suspend(self.mode) 44 | -------------------------------------------------------------------------------- /ecofreq.cfg.templ: -------------------------------------------------------------------------------- 1 | [general] 2 | #LogCO2extra=true 3 | #LogCost=true 4 | 5 | [provider] 6 | all=electricitymaps 7 | # fixed energy price: 30 ct/kWh 8 | price=const:30 9 | #price=tibber 10 | Interval=600 11 | 12 | [electricitymaps] 13 | Zone= 14 | #Token= 15 | DisableEstimations=False 16 | EmissionFactorType=lifecycle 17 | 18 | [energycharts] 19 | #List of available countries/price zones: https://api.energy-charts.info/#/ 20 | Country= 21 | PostCode= 22 | PriceZone= 23 | 24 | [ukgrid] 25 | PostCode= 26 | #RegionID=10 27 | 28 | [stromgedacht] 29 | PostCode= 30 | #IntegerStates=True 31 | 32 | [watttime] 33 | username=myuser 34 | password=mypwd 35 | zone= 36 | 37 | [tibber] 38 | Token=TIBBER_TOKEN 39 | #UseCache=True 40 | 41 | [gridstatus.io] 42 | # Get API token here: https://www.gridstatus.io/api 43 | Token=YOUR_TOKEN 44 | ISO=ercot 45 | Location=HB_NORTH 46 | # Available datasets: https://www.gridstatus.io/datasets 47 | #Dataset=ercot_spp_day_ahead_hourly 48 | #PriceField=spp 49 | 50 | [policy] 51 | Control=auto 52 | Metric=co2 53 | DefaultGovernor=step:200=0.8:400=0.6 54 | Governor= 55 | 56 | [monitor] 57 | PowerSensor=auto 58 | Interval=5 59 | 60 | [idle] 61 | #IdleMonitor=off 62 | LoadCutoff=0.05 63 | # 1 = 1min average, 2 = 5min, 3 = 15min 64 | LoadPeriod=1 65 | # Switch to low-energy standby mode (suspend) after x seconds of idling 66 | #SuspendAfter=900 67 | -------------------------------------------------------------------------------- /config/ecofreq.cfg.stromgedacht: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use StromGedacht API: 8 | all=stromgedacht 9 | # CO2 Option1: use CO2Signal for carbon accounting (API token required!) 10 | #co2=co2signal 11 | # CO2 Option2: use constant carbon intensity (gCO2e/kwh) 12 | co2=const:350 13 | # Price Option1: wholesale day-ahead market price by EnergyCharts 14 | price=energycharts 15 | # Price Option2: constant price in ct/kwh 16 | #price=const:30 17 | # Price Option3: dynamic price (tibber, octopus or awattar) 18 | #price=tibber 19 | 20 | [stromgedacht] 21 | PostCode=69118 22 | #IntegerStates=True 23 | 24 | [co2signal] 25 | #Please get your free API token here: https://co2signal.com/ 26 | Token=YOUR_TOKEN 27 | #Please look up your grid zone here: https://www.electricitymap.org 28 | Country=DE 29 | 30 | [energycharts] 31 | #List of available countries/price zones: https://api.energy-charts.info/#/ 32 | Country=DE 33 | PostCode=69118 34 | PriceZone=DE-LU 35 | 36 | [tibber] 37 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 38 | UseCache=True 39 | 40 | [policy] 41 | Control=auto 42 | # No dynamic power scaling by default: 43 | #DefaultGovernor=maxperf 44 | # Sample powercap policy: red(grid congestion) + orange(low RE) -> 50%, greed(normal operation) -> 70%, supergreen(RE surplus) -> 100% 45 | Metric=index 46 | DefaultGovernor=list:red=0.5:orange=0.5:green=0.7:supergreen=max 47 | Governor=default 48 | 49 | [monitor] 50 | PowerSensor=auto 51 | Interval=5 52 | -------------------------------------------------------------------------------- /ecofreq/config/stromgedacht.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use StromGedacht API: 8 | all=stromgedacht 9 | # CO2 Option1: use CO2Signal for carbon accounting (API token required!) 10 | #co2=co2signal 11 | # CO2 Option2: use constant carbon intensity (gCO2e/kwh) 12 | co2=const:350 13 | # Price Option1: wholesale day-ahead market price by EnergyCharts 14 | price=energycharts 15 | # Price Option2: constant price in ct/kwh 16 | #price=const:30 17 | # Price Option3: dynamic price (tibber, octopus or awattar) 18 | #price=tibber 19 | 20 | [stromgedacht] 21 | PostCode=69118 22 | #IntegerStates=True 23 | 24 | [co2signal] 25 | #Please get your free API token here: https://co2signal.com/ 26 | Token=YOUR_TOKEN 27 | #Please look up your grid zone here: https://www.electricitymap.org 28 | Country=DE 29 | 30 | [energycharts] 31 | #List of available countries/price zones: https://api.energy-charts.info/#/ 32 | Country=DE 33 | PostCode=69118 34 | PriceZone=DE-LU 35 | 36 | [tibber] 37 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 38 | UseCache=True 39 | 40 | [policy] 41 | Control=auto 42 | # No dynamic power scaling by default: 43 | #DefaultGovernor=maxperf 44 | # Sample powercap policy: red(grid congestion) + orange(low RE) -> 50%, greed(normal operation) -> 70%, supergreen(RE surplus) -> 100% 45 | Metric=index 46 | DefaultGovernor=list:red=0.5:orange=0.5:green=0.7:supergreen=max 47 | Governor=default 48 | 49 | [monitor] 50 | PowerSensor=auto 51 | Interval=5 52 | -------------------------------------------------------------------------------- /ecofreq/helpers/docker.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output,DEVNULL,CalledProcessError 2 | 3 | class DockerHelper(object): 4 | CMD_DOCKER = "docker" 5 | 6 | @classmethod 7 | def available(cls): 8 | try: 9 | out = cls.run_cmd(["-v"]) 10 | #TODO check version 11 | return True 12 | except CalledProcessError: 13 | return False 14 | 15 | @classmethod 16 | def run_cmd(cls, args, parse_output=True): 17 | cmdline = cls.CMD_DOCKER + " " + " ".join(args) 18 | # print(cmdline) 19 | out = check_output(cmdline, shell=True, stderr=DEVNULL, universal_newlines=True) 20 | result = [] 21 | if parse_output: 22 | for line in out.split("\n"): 23 | if line: 24 | if not line.startswith("Emulate Docker CLI"): 25 | result.append([x.strip() for x in line.split(",")]) 26 | return result 27 | 28 | @classmethod 29 | def get_container_ids(cls): 30 | out = cls.run_cmd(["ps", "--format", "{{.ID}}"]) 31 | ids = [x[0] for x in out] 32 | return ids 33 | 34 | @classmethod 35 | def set_container_cpus(cls, ctrs, cpus): 36 | if not ctrs: 37 | ctrs = cls.get_container_ids() 38 | for c in ctrs: 39 | cls.run_cmd(["container", "update", "--cpus", str(cpus), c], False) 40 | 41 | @classmethod 42 | def set_pause(cls, ctrs, pause=True): 43 | args = ["pause" if pause else "unpause"] 44 | if not ctrs: 45 | args += ["-a"] 46 | else: 47 | args += ctrs 48 | cls.run_cmd(args, False) 49 | 50 | -------------------------------------------------------------------------------- /config/ecofreq.cfg.energycharts: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use EnergyChart electricity traffic light: https://api.energy-charts.info/ 8 | all=energycharts 9 | # CO2 Option1: use ElectricityMaps for carbon accounting 10 | co2=electricitymaps 11 | # CO2 Option2: use constant carbon intensity (gCO2e/kwh) 12 | #co2=const:350 13 | # Price Option1 (default): wholesale day-ahead market price by EnergyCharts 14 | # Price Option2: constant price in ct/kwh 15 | #price=const:30 16 | # Price Option3: dynamic price (tibber, octopus or awattar) 17 | #price=tibber 18 | 19 | [energycharts] 20 | #List of available countries/price zones: https://api.energy-charts.info/#/ 21 | Country=DE 22 | PostCode=69118 23 | PriceZone=DE-LU 24 | 25 | [electricitymaps] 26 | Zone=DE 27 | #Token= 28 | DisableEstimations=False 29 | EmissionFactorType=lifecycle 30 | 31 | [awattar] 32 | # Awattar is available in Germany (DE) and Austria (AT), see https://www.awattar.at/ 33 | Country=DE 34 | FixedPrice=15.43 35 | VAT=0.19 36 | 37 | [tibber] 38 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 39 | UseCache=True 40 | 41 | [octopus] 42 | Product=AGILE-18-02-21 43 | Tariff=E-1R-AGILE-18-02-21-C 44 | UseCache=F 45 | 46 | [policy] 47 | Control=auto 48 | # No dynamic power scaling by default: 49 | #DefaultGovernor=maxperf 50 | # Sample powercap policy: black(grid congestion) or red(low RE) -> 50%, yellow(medium RE) -> 70%, green(high RE) -> 100% 51 | Metric=index 52 | DefaultGovernor=list:black=0.5:red=0.5:yellow=0.7:green=max 53 | Governor=default 54 | 55 | [monitor] 56 | PowerSensor=auto 57 | Interval=5 58 | -------------------------------------------------------------------------------- /ecofreq/config/energycharts.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | logco2extra=true 3 | 4 | [provider] 5 | # Query interval in seconds 6 | Interval=600 7 | # Use EnergyChart electricity traffic light: https://api.energy-charts.info/ 8 | all=energycharts 9 | # CO2 Option1: use ElectricityMaps for carbon accounting 10 | #co2=electricitymaps 11 | # CO2 Option2: use constant carbon intensity (gCO2e/kwh) 12 | #co2=const:350 13 | # Price Option1 (default): wholesale day-ahead market price by EnergyCharts 14 | # Price Option2: constant price in ct/kwh 15 | #price=const:30 16 | # Price Option3: dynamic price (tibber, octopus or awattar) 17 | #price=tibber 18 | 19 | [energycharts] 20 | #List of available countries/price zones: https://api.energy-charts.info/#/ 21 | Country=DE 22 | PostCode=69118 23 | PriceZone=DE-LU 24 | 25 | [electricitymaps] 26 | Zone=DE 27 | #Token= 28 | DisableEstimations=False 29 | EmissionFactorType=lifecycle 30 | 31 | [awattar] 32 | # Awattar is available in Germany (DE) and Austria (AT), see https://www.awattar.at/ 33 | Country=DE 34 | FixedPrice=15.43 35 | VAT=0.19 36 | 37 | [tibber] 38 | Token=5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 39 | UseCache=True 40 | 41 | [octopus] 42 | Product=AGILE-18-02-21 43 | Tariff=E-1R-AGILE-18-02-21-C 44 | UseCache=F 45 | 46 | [policy] 47 | Control=auto 48 | # No dynamic power scaling by default: 49 | #DefaultGovernor=maxperf 50 | # Sample powercap policy: black(grid congestion) or red(low RE) -> 50%, yellow(medium RE) -> 70%, green(high RE) -> 100% 51 | Metric=index 52 | DefaultGovernor=list:black=0.5:red=0.5:yellow=0.7:green=max 53 | Governor=default 54 | 55 | [monitor] 56 | PowerSensor=auto 57 | Interval=5 58 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Which real-time CO2 and price provider to use? 2 | 3 | We recommend following region-specific APIs, and provide sample config files for them: 4 | 5 | ## CO2 6 | 7 | - Great Britain (UK): [National Grid ESO](https://carbonintensity.org.uk/) -> [ecofreq.cfg.ukgrid](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.ukgrid) 8 | - Continental Europe: [EnergyCharts](https://energy-charts.info/) -> [ecofreq.cfg.energycharts](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.energycharts) or 9 | [ElectricityMaps](https://static.electricitymaps.com/api/docs/index.html) -> [ecofreq.cfg.electricitymaps](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.electricitymaps) 10 | - Germany, Baden-Württemberg: [StromGedacht](https://www.stromgedacht.de/) -> [ecofreq.cfg.stromgedacht](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.stromgedacht) 11 | - US: [WattTime](https://www.watttime.org/) -> [ecofreq.cfg.watttime](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.watttime) 12 | - Rest of the world: [ElectricityMaps/CO2Signal](https://static.electricitymaps.com/api/docs/index.html) -> [ecofreq.cfg.co2signal](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.co2signal) 13 | 14 | ## Price (wholesale) 15 | 16 | - Continental Europe: [EnergyCharts](https://energy-charts.info/) 17 | - US: [GridStatus.io](https://api.gridstatus.io/docs) -> [ecofreq.cfg.watttime](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.watttime) 18 | 19 | ## Price (retail) 20 | 21 | - UK: [Octopus](https://octopus.energy/) 22 | - Germany: [Tibber](https://tibber.com) 23 | - Germany, Austria: [Awattar](https://www.awattar.at/) 24 | -------------------------------------------------------------------------------- /doc/POLICY.md: -------------------------------------------------------------------------------- 1 | # Policy = Governor + Control method 2 | 3 | In EcoFreq, dynamic power scaling policy consists of two components: 4 | - Governor is a formula to convert the input signal (e.g., `co2` or `price`) to the capacity limit value 5 | - Control method defines how capacity limit is applied (e.g., `power` or `frequency` cap) 6 | 7 | ## Control methods 8 | 9 | ### `power` cap 10 | ``` 11 | [policy] 12 | control=power 13 | ``` 14 | This method defines direct power cap for CPU or GPU. 15 | Supported hardware: 16 | * Intel CPUs (Sandy Bridge and later, ca. 2012+) - via RAPL 17 | * AMD CPUs ([Zen3/Zen4 and later](https://github.com/amd/esmi_ib_library?tab=readme-ov-file#supported-hardware)) - via [E-SMI](https://github.com/amd/esmi_ib_library), `e_smi_tool` must be installed under `/opt/e-sms/e_smi/bin/` (default) 18 | * NVIDIA GPUs (most modern consumer and HPC cards) - via `nvidia-smi -pl` 19 | 20 | 21 | ### `frequency` cap (DVFS) 22 | ``` 23 | [policy] 24 | control=frequency 25 | ``` 26 | 27 | ### utilization cap (`cgroup`) 28 | ``` 29 | [cpu_policy] 30 | control=cgroup 31 | cgroup=ef 32 | ``` 33 | 34 | 35 | ### utilization cap (`docker`) -> EXPERIMENTAL 36 | ``` 37 | [cpu_policy] 38 | control=docker 39 | containers=my_cool_container,e8f7089b12f1 40 | ``` 41 | 42 | 43 | 44 | ## Governors 45 | 46 | * Constant (`const`) 47 | 48 | ``` 49 | const:80% 50 | const:2000MHz 51 | const:150W 52 | const:12c 53 | ``` 54 | 55 | * Discrete (`list`) 56 | 57 | ``` 58 | list:black=0.5:red=0.5:yellow=0.7:green=max 59 | list:very low=max:low=max:moderate=0.8:high=0.6:very high=0.6 60 | ``` 61 | 62 | * Step function (`step`) 63 | 64 | ``` 65 | step:100=70%:200=50% 66 | ``` 67 | 68 | * Linear function (`linear`) 69 | 70 | ``` 71 | linear:100:500 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /ecofreq/config/README.md: -------------------------------------------------------------------------------- 1 | # Which real-time CO2 and price provider to use? 2 | 3 | We recommend following region-specific APIs, and provide sample config files for them: 4 | 5 | ## CO2 6 | 7 | - Great Britain (UK): [National Grid ESO](https://carbonintensity.org.uk/) -> [ecofreq.cfg.ukgrid](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.ukgrid) 8 | - Continental Europe: [EnergyCharts](https://energy-charts.info/) -> [ecofreq.cfg.energycharts](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.energycharts) or 9 | [ElectricityMaps](https://static.electricitymaps.com/api/docs/index.html) -> [ecofreq.cfg.electricitymaps](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.electricitymaps) 10 | - Germany, Baden-Württemberg: [StromGedacht](https://www.stromgedacht.de/) -> [ecofreq.cfg.stromgedacht](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.stromgedacht) 11 | - US: [WattTime](https://www.watttime.org/) -> [ecofreq.cfg.watttime](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.watttime) 12 | - Rest of the world: [ElectricityMaps/CO2Signal](https://static.electricitymaps.com/api/docs/index.html) -> [ecofreq.cfg.co2signal](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.co2signal) 13 | 14 | ## Price (wholesale) 15 | 16 | - Continental Europe: [EnergyCharts](https://energy-charts.info/) 17 | - US: [GridStatus.io](https://api.gridstatus.io/docs) -> [ecofreq.cfg.watttime](https://github.com/amkozlov/eco-freq/blob/main/config/ecofreq.cfg.watttime) 18 | 19 | ## Price (retail) 20 | 21 | - UK: [Octopus](https://octopus.energy/) 22 | - Germany: [Tibber](https://tibber.com) 23 | - Germany, Austria: [Awattar](https://www.awattar.at/) 24 | -------------------------------------------------------------------------------- /ecofreq/monitors/freq.py: -------------------------------------------------------------------------------- 1 | from ecofreq.monitors.common import Monitor 2 | from ecofreq.helpers.cpu import CpuFreqHelper 3 | from ecofreq.config import OPTION_DISABLED 4 | 5 | class FreqMonitor(Monitor): 6 | def __init__(self, config): 7 | Monitor.__init__(self, config) 8 | self.period_freq = 0 9 | self.last_freq = 0 10 | 11 | def reset_period(self): 12 | Monitor.reset_period(self) 13 | self.period_freq = 0 14 | self.last_freq = 0 15 | 16 | @classmethod 17 | def from_config(cls, config): 18 | sens_dict = { "cpu" : CPUFreqMonitor } 19 | p = config["monitor"].get("FreqSensor", "auto").lower() 20 | monitors = [] 21 | if p in OPTION_DISABLED: 22 | pass 23 | elif p == "auto": 24 | if CPUFreqMonitor.available(): 25 | monitors.append(CPUFreqMonitor(config)) 26 | else: 27 | for s in p.split(","): 28 | if s in sens_dict: 29 | monitors.append(sens_dict[s](config)) 30 | else: 31 | raise ValueError("Unknown frequency sensor: " + p) 32 | return monitors 33 | 34 | class CPUFreqMonitor(FreqMonitor): 35 | def __init__(self, config): 36 | FreqMonitor.__init__(self, config) 37 | 38 | @classmethod 39 | def available(cls): 40 | return CpuFreqHelper.available() 41 | 42 | def update_freq(self): 43 | avg_freq = CpuFreqHelper.get_avg_gov_cur_freq() 44 | frac_new = 1. / (self.period_samples + 1) 45 | frac_old = self.period_samples * frac_new 46 | self.period_freq = frac_old * self.period_freq + frac_new * avg_freq 47 | self.last_freq = avg_freq 48 | 49 | def update_impl(self): 50 | self.update_freq() 51 | 52 | def get_period_avg_freq(self, unit=CpuFreqHelper.KHZ): 53 | return self.period_freq / unit 54 | 55 | def get_last_avg_freq(self, unit=CpuFreqHelper.KHZ): 56 | return self.last_freq / unit 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /doc/INSTALL.md: -------------------------------------------------------------------------------- 1 | # EcoFreq installation guide 2 | 3 | ## Prerequisites 4 | 5 | - Linux system (tested with Ubuntu and CentOS) 6 | - Python3.8+ with `pip` (`pipx` recommended) 7 | - (optional) API token -> [Which real-time CO2/price provider to use?](https://github.com/amkozlov/eco-freq/blob/main/config/README.md/) 8 | - (optional) [`ipmitool`](https://github.com/ipmitool/ipmitool) to use IPMI power measurements 9 | 10 | ## Python package 11 | 12 | First, install EcoFreq package using `pip` or `pipx`: 13 | 14 | ``` 15 | pipx install ecofreq 16 | ``` 17 | 18 | This will install EcoFreq locally under the current (non-privileged) user. Since power limiting typically requires root privileges, you will need sudo permissions to use all features of EcoFreq ([see details](https://github.com/amkozlov/eco-freq/blob/main/doc/INSTALL.md#Permissions)). 19 | 20 | Alternatively, you can install and run EcoFreq under root: 21 | ``` 22 | sudo pipx install ecofreq 23 | ``` 24 | This is less secure, but makes configuration simpler. 25 | 26 | ## Configuration 27 | 28 | Example configuration files are available under: https://github.com/amkozlov/eco-freq/tree/main/config 29 | 30 | You can easily create a config file from template using `showcfg` command, e.g. 31 | ``` 32 | ecofreq -c energycharts showcfg > myecofreq.cfg 33 | ``` 34 | You can then customize `myecofreq.cfg` by specifying your region, carbon intensity and price provider, API tokens etc. 35 | For details, please see [CONFIG.md](https://github.com/amkozlov/eco-freq/blob/main/doc/CONFIG.md/) 36 | 37 | ## Daemon 38 | 39 | Then, please run installer command which will register `systemd` service and configure permissions for EcoFreq: 40 | 41 | ``` 42 | ecofreq -c myecofreq.cfg install 43 | ``` 44 | 45 | Check that EcoFreq daemon is up and running: 46 | ``` 47 | ecoctl 48 | ``` 49 | 50 | You can disable `systemd` service and remove all respective files by running 51 | 52 | ``` 53 | ecofreq remove 54 | ``` 55 | 56 | 57 | ## Permissions 58 | 59 | By default, EcoFreq will try to obtain root permissions (`sudo`) which are (unfortunately) required for most power scaling methods. 60 | To ensure it also works for the daemon, the installation process described above will automatically create the respective `/etc/sudpers.d/ecofreq` file. 61 | 62 | If this is not possible or not desired, you can enforce rootless mode with 63 | 64 | ``` 65 | ecofreq --user 66 | ``` 67 | 68 | Please note, however, that EcoFreq funtionality will be limited in rootless mode. 69 | 70 | These limitations can be avoided/relaxed by adjusting permissions for the relevant system files and utilities: TODO 71 | -------------------------------------------------------------------------------- /ecofreq/policy/manager.py: -------------------------------------------------------------------------------- 1 | from ecofreq.providers.common import EcoProvider 2 | from ecofreq.policy.cpu import CPUEcoPolicy 3 | from ecofreq.policy.gpu import GPUEcoPolicy 4 | 5 | class EcoPolicyManager(object): 6 | def __init__(self, config): 7 | self.policies = [] 8 | cfg_dict = {"cpu": None, "gpu": None} 9 | if "policy" in config: 10 | cfg_dict["gpu"] = cfg_dict["cpu"] = dict(config.items("policy")) 11 | if "cpu_policy" in config: 12 | cfg_dict["cpu"] = dict(config.items("cpu_policy")) 13 | if "gpu_policy" in config: 14 | cfg_dict["gpu"] = dict(config.items("gpu_policy")) 15 | cfg_dict["metric"] = cfg_dict["cpu"].get("metric", "co2") 16 | self.set_config(cfg_dict) 17 | 18 | def info_string(self): 19 | if self.policies: 20 | s = [p.info_string() for p in self.policies] 21 | s.append("metric = " + self.metric) 22 | return ", ".join(s) 23 | else: 24 | return "None" 25 | 26 | def clear(self): 27 | self.reset() 28 | self.policies = [] 29 | 30 | def set_config(self, cfg): 31 | if not cfg: 32 | self.clear() 33 | return 34 | self.metric = cfg.get("metric", "co2") 35 | if "cpu" in cfg or "gpu" in cfg: 36 | all_cfg = None 37 | else: 38 | all_cfg = cfg 39 | cpu_cfg = cfg.get("cpu", all_cfg) 40 | gpu_cfg = cfg.get("gpu", all_cfg) 41 | cpu_pol = CPUEcoPolicy.from_config(cpu_cfg) 42 | gpu_pol = GPUEcoPolicy.from_config(gpu_cfg) 43 | self.clear() 44 | if cpu_pol: 45 | self.policies.append(cpu_pol) 46 | if gpu_pol: 47 | self.policies.append(gpu_pol) 48 | 49 | def get_config(self): 50 | res = {} 51 | for p in self.policies: 52 | domain = "global" 53 | if issubclass(type(p), CPUEcoPolicy): 54 | domain = "cpu" 55 | elif issubclass(type(p), GPUEcoPolicy): 56 | domain = "gpu" 57 | res[domain] = p.get_config({}) 58 | res[domain]["metric"] = self.metric 59 | return res 60 | 61 | def set_co2(self, co2_data): 62 | if self.metric == "price": 63 | field = EcoProvider.FIELD_PRICE 64 | elif self.metric == "fossil_pct": 65 | field = EcoProvider.FIELD_FOSSIL_PCT 66 | elif self.metric == "ren_pct": 67 | field = EcoProvider.FIELD_REN_PCT 68 | elif self.metric == "index": 69 | field = EcoProvider.FIELD_INDEX 70 | else: 71 | field = EcoProvider.FIELD_CO2 72 | if not co2_data[field] is None: 73 | val = co2_data[field] 74 | for p in self.policies: 75 | p.set_co2(val) 76 | 77 | def reset(self): 78 | for p in self.policies: 79 | p.reset() 80 | -------------------------------------------------------------------------------- /ecofreq/installer/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | scriptdir=`readlink -f $0 | xargs dirname` 4 | servicehome=/lib/systemd/system 5 | sudoersfile=/etc/sudoers.d/ecofreq 6 | 7 | uninstall=0 8 | usesudo=1 9 | ecocmd=`readlink -f $scriptdir/../ecofreq.py` 10 | #ecouser=$logname 11 | ecogroup=ecofreq 12 | userline= 13 | 14 | usage() 15 | { 16 | echo "Usage: ./install.sh -e SCRIPT [-c CONFIG] [-u USER] [-g GROUP] [-n]" 17 | echo -e "\nOptions:" 18 | echo -e "\t-e SCRIPT Full absolute path to the ecofreq.py (e.g. /home/user/.local/bin/ecofreq.py)" 19 | echo -e "\t-c CONFIG Configuration file" 20 | echo -e "\t-u USER User under which EcoFreq daemon will be started" 21 | echo -e "\t-g GROUP Group with access to ecoctl (default: ecofreq)" 22 | echo -e "\t-n Run without sudo" 23 | } 24 | 25 | #parse options 26 | while getopts "h?e:c:u:g:nU" opt; do 27 | case "$opt" in 28 | h|\?) 29 | usage 30 | exit 0 31 | ;; 32 | e) ecocmd=$OPTARG 33 | ;; 34 | c) cfgfile=$OPTARG 35 | ;; 36 | u) ecouser=${OPTARG:-$logname} 37 | ;; 38 | g) ecogroup=$OPTARG 39 | ;; 40 | n) usesudo=0 41 | ;; 42 | U) uninstall=1 43 | ;; 44 | esac 45 | done 46 | 47 | if [ $uninstall -eq 1 ]; then 48 | rm $sudoersfile 49 | 50 | systemctl stop ecofreq 51 | systemctl disable ecofreq 52 | rm $servicehome/ecofreq.service 53 | 54 | echo -e "EcoFreq uninstalled!" 55 | 56 | exit 57 | fi 58 | 59 | 60 | if [ ! -z $cfgfile ]; then 61 | cfgabs=`readlink -f $cfgfile` 62 | ecocmd="$ecocmd -c $cfgabs" 63 | fi 64 | 65 | echo -e "Step 1: Create users and groups...\n" 66 | 67 | if [ ! -z $ecogroup ]; then 68 | groupadd -f $ecogroup 69 | fi 70 | 71 | if [ ! -z $ecouser ]; then 72 | 73 | ecogroup=${ecogroup:-ecofreq} 74 | 75 | usermod -aG $ecogroup $ecouser 76 | 77 | echo -e "Step 2: Add sudoers.d file to run ecofreq daemon without password...\n" 78 | 79 | echo "$ecouser ALL = (root:$ecogroup) NOPASSWD: $ecocmd" > $sudoersfile 80 | 81 | userline="User=$ecouser" 82 | else 83 | usesudo=0 84 | fi 85 | 86 | echo -e "Step 3: Register systemd service...\n" 87 | 88 | if [ $usesudo -eq 1 ]; then 89 | ecocmd="sudo $ecocmd" 90 | fi 91 | 92 | sed -e "s##$ecocmd#" -e "s#User=#$userline#" -e "s##$ecogroup#" $scriptdir/ecofreq.service > $servicehome/ecofreq.service 93 | 94 | systemctl daemon-reload 95 | 96 | systemctl enable ecofreq 97 | 98 | systemctl start ecofreq 99 | 100 | echo -e "\nInstallation complete!\n" 101 | -------------------------------------------------------------------------------- /ecofreq/helpers/nvidia.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output,DEVNULL,CalledProcessError 2 | 3 | class NvidiaGPUHelper(object): 4 | CMD_NVSMI = "nvidia-smi" 5 | 6 | @classmethod 7 | def available(cls): 8 | # return call(cls.CMD_NVSMI, shell=True, stdout=DEVNULL, stderr=DEVNULL) == 0 9 | try: 10 | out = cls.query_gpus(fields = "power.draw,power.management") 11 | # print (out) 12 | return "Enabled" in out[0][1] 13 | except CalledProcessError: 14 | return False 15 | 16 | @classmethod 17 | def query_gpus(cls, fields, fmt = "csv,noheader,nounits", qcmd="--query-gpu"): 18 | cmdline = cls.CMD_NVSMI + " --format=" + fmt + " " + qcmd + "=" + fields 19 | out = check_output(cmdline, shell=True, stderr=DEVNULL, universal_newlines=True) 20 | result = [] 21 | for line in out.split("\n"): 22 | if line: 23 | result.append([x.strip() for x in line.split(",")]) 24 | return result 25 | 26 | @classmethod 27 | def get_power(cls): 28 | pwr = [ float(x[0]) for x in cls.query_gpus(fields = "power.draw") ] 29 | return sum(pwr) 30 | 31 | @classmethod 32 | def get_power_limit(cls): 33 | pwr = [ float(x[0]) for x in cls.query_gpus(fields = "power.limit") ] 34 | return sum(pwr) 35 | 36 | @classmethod 37 | def get_power_limit_all(cls): 38 | return cls.query_gpus(fields = "power.min_limit,power.max_limit,power.limit") 39 | 40 | @classmethod 41 | def set_power_limit(cls, max_gpu_power): 42 | cmdline = cls.CMD_NVSMI + " -pl " + str(max_gpu_power) 43 | out = check_output(cmdline, shell=True, stderr=DEVNULL, universal_newlines=True) 44 | 45 | @classmethod 46 | def get_supported_freqs(cls): 47 | return cls.query_gpus(fields="graphics", qcmd="--query-supported-clocks") 48 | 49 | @classmethod 50 | def get_hw_max_freq(cls): 51 | return [float(x[0]) for x in cls.query_gpus(fields = "clocks.max.gr")] 52 | 53 | @classmethod 54 | def set_freq_limit(cls, max_gpu_freq): 55 | cmdline = cls.CMD_NVSMI + " -lgc 0," + str(int(max_gpu_freq)) 56 | cmdline += " --mode=1" 57 | out = check_output(cmdline, shell=True, stderr=DEVNULL, universal_newlines=True) 58 | 59 | @classmethod 60 | def reset_freq_limit(cls): 61 | cmdline = cls.CMD_NVSMI + " -rgc" 62 | out = check_output(cmdline, shell=True, stderr=DEVNULL, universal_newlines=True) 63 | 64 | @classmethod 65 | def info(cls): 66 | if cls.available(): 67 | field_list = "name,power.min_limit,power.max_limit,power.limit" 68 | cnt = 0 69 | for gi in cls.query_gpus(fields = field_list, fmt="csv,noheader"): 70 | print ("GPU" + str(cnt) + ": " + gi[0] + ", min_hw_limit = " + gi[1] + ", max_hw_limit = " + gi[2] + ", current_limit = " + gi[3]) 71 | cnt += 1 72 | -------------------------------------------------------------------------------- /ecofreq/ipc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import json 4 | 5 | class EcoServer(object): 6 | IPC_FILE="/var/run/ecofreq.sock" 7 | BUF_SIZE=2048 8 | 9 | def __init__(self, iface, config=None): 10 | import grp 11 | self.iface = iface 12 | self.fmod = 0o660 13 | gname = "ecofreq" 14 | if config and "server" in config: 15 | gname = config["server"].get("filegroup", gname) 16 | if "filemode" in config["server"]: 17 | self.fmod = int(config["server"]["filemode"], 8) 18 | try: 19 | self.gid = grp.getgrnam(gname).gr_gid 20 | except KeyError: 21 | self.gid = -1 22 | 23 | async def spin(self): 24 | self.serv = await asyncio.start_unix_server(self.on_connect, path=self.IPC_FILE) 25 | if self.gid >= 0: 26 | os.chown(self.IPC_FILE, -1, self.gid) 27 | os.chmod(self.IPC_FILE, self.fmod) 28 | 29 | # print(f"Server init") 30 | # async with self.serv: 31 | await self.serv.serve_forever() 32 | 33 | async def on_connect(self, reader, writer): 34 | data = await reader.read(self.BUF_SIZE) 35 | msg = data.decode() 36 | # addr = writer.get_extra_info('peername') 37 | 38 | # print(f"Received {msg!r}") 39 | 40 | try: 41 | req = json.loads(msg) 42 | cmd = req['cmd'] 43 | args = req['args'] if 'args' in req else {} 44 | res = self.iface.run_cmd(cmd, args) 45 | response = json.dumps(res) 46 | except: 47 | response = "Invalid message" 48 | 49 | writer.write(response.encode()) 50 | await writer.drain() 51 | writer.close() 52 | 53 | class EcoClient(object): 54 | 55 | async def unix_send(self, message): 56 | try: 57 | reader, writer = await asyncio.open_unix_connection(EcoServer.IPC_FILE) 58 | except FileNotFoundError: 59 | raise ConnectionRefusedError 60 | 61 | # print(f'Send: {message!r}') 62 | writer.write(message.encode()) 63 | await writer.drain() 64 | 65 | data = await reader.read(EcoServer.BUF_SIZE) 66 | # print(f'Received: {data.decode()!r}') 67 | 68 | writer.close() 69 | 70 | return data.decode() 71 | 72 | def send_cmd(self, cmd, args=None): 73 | obj = dict(cmd=cmd, args=args) 74 | msg = json.dumps(obj) 75 | resp = asyncio.run(self.unix_send(msg)) 76 | try: 77 | return json.loads(resp) 78 | except: 79 | return dict(status='ERROR', error='Exception') 80 | 81 | def info(self): 82 | return self.send_cmd('info') 83 | 84 | def get_policy(self): 85 | return self.send_cmd('get_policy') 86 | 87 | def set_policy(self, policy): 88 | return self.send_cmd('set_policy', policy) 89 | 90 | def get_provider(self): 91 | return self.send_cmd('get_provider') 92 | 93 | def set_provider(self, provider): 94 | return self.send_cmd('set_provider', provider) 95 | 96 | -------------------------------------------------------------------------------- /ecofreq/mqtt.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | 4 | try: 5 | from aiomqtt import Client, MqttError 6 | mqtt_found = True 7 | except: 8 | mqtt_found = False 9 | 10 | class MQTTClient(object): 11 | recv_queue: asyncio.Queue 12 | send_queue: asyncio.Queue 13 | 14 | def __init__(self, config): 15 | self.hostname = config.get("host", "localhost") 16 | self.port = None 17 | self.username = None 18 | self.password = None 19 | self.sub_topic = config.get("topic", None) 20 | self.pub_topic = config.get("pubtopic", None) 21 | self.pub_fields = config.get("pubfields", None) 22 | if self.pub_fields: 23 | self.pub_fields = self.pub_fields.split(",") 24 | 25 | async def run(self): 26 | self.recv_queue = asyncio.Queue() 27 | self.send_queue = asyncio.Queue() 28 | while True: 29 | print('Connecting to MQTT broker...') 30 | try: 31 | async with Client( 32 | hostname=self.hostname, 33 | # port=self.port, 34 | username=self.username, 35 | password=self.password 36 | ) as client: 37 | print('Connected to MQTT broker') 38 | 39 | # Handle pub/sub 40 | await asyncio.gather( 41 | self.handle_sub(client), 42 | self.handle_pub(client) 43 | ) 44 | except MqttError: 45 | print('MQTT error:') 46 | await asyncio.sleep(5) 47 | 48 | async def handle_sub(self, client): 49 | if not self.sub_topic: 50 | return 51 | await client.subscribe(self.sub_topic) 52 | async for message in client.messages: 53 | self.recv_queue.put_nowait(message.payload) 54 | 55 | async def handle_pub(self, client): 56 | if not self.pub_topic: 57 | return 58 | while True: 59 | data = await self.send_queue.get() 60 | payload = json.dumps(data) 61 | # print(payload) 62 | await client.publish(self.pub_topic, payload=payload.encode()) 63 | self.send_queue.task_done() 64 | 65 | def get_msg(self): 66 | try: 67 | return self.recv_queue.get_nowait() 68 | except (asyncio.queues.QueueEmpty, AttributeError): 69 | return None 70 | 71 | def put_msg(self, data): 72 | if self.pub_fields: 73 | data = {key: data[key] for key in self.pub_fields} 74 | self.send_queue.put_nowait(data) 75 | 76 | class MQTTManager(object): 77 | CLMAP = {} 78 | 79 | @classmethod 80 | def add_client(cls, label, config): 81 | client = MQTTClient(config) 82 | cls.CLMAP[label] = client 83 | return client 84 | 85 | @classmethod 86 | def get_client(cls, label): 87 | return cls.CLMAP[label] 88 | 89 | @classmethod 90 | async def run(cls): 91 | tasks = [asyncio.create_task(c.run()) for c in cls.CLMAP.values()] 92 | for t in tasks: 93 | await t 94 | -------------------------------------------------------------------------------- /ecofreq/monitors/idle.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output,DEVNULL 2 | 3 | from ecofreq.utils import * 4 | from ecofreq.monitors.common import Monitor 5 | from ecofreq.config import OPTION_DISABLED 6 | 7 | class IdleMonitor(Monitor): 8 | CMD_SESSION_COUNT="w -h | wc -l" 9 | LOADAVG_FILE="/proc/loadavg" 10 | LOADAVG_M1=1 11 | LOADAVG_M5=2 12 | LOADAVG_M15=3 13 | 14 | @classmethod 15 | def from_config(cls, config): 16 | monitors = [] 17 | p = "on" 18 | if config.has_section("idle"): 19 | p = config["idle"].get("IdleMonitor", p).lower() 20 | if p not in OPTION_DISABLED: 21 | monitors.append(IdleMonitor(config)) 22 | return monitors 23 | 24 | def __init__(self, config): 25 | Monitor.__init__(self, config) 26 | c = config['idle'] if 'idle' in config else {} 27 | self.load_cutoff = float(c.get('LoadCutoff', 0.05)) 28 | self.load_period = int(c.get('LoadPeriod', 1)) 29 | if self.load_period not in [self.LOADAVG_M1, self.LOADAVG_M5, self.LOADAVG_M15]: 30 | raise ValueError("IdleMonitor: Unknown load period: " + self.load_period) 31 | self.reset() 32 | 33 | def reset_period(self): 34 | Monitor.reset_period(self) 35 | self.max_sessions = 0 36 | self.max_load = 0. 37 | 38 | def reset(self): 39 | self.idle_duration = 0 40 | self.last_sessions = 0 41 | self.last_load = 0. 42 | self.reset_period() 43 | 44 | def active_sessions(self): 45 | out = check_output(self.CMD_SESSION_COUNT, shell=True, stderr=DEVNULL, universal_newlines=True) 46 | return int(out) 47 | 48 | def active_load(self): 49 | return float(read_value(self.LOADAVG_FILE, self.load_period)) 50 | 51 | def update_impl(self): 52 | self.last_sessions = self.active_sessions() 53 | self.last_load = self.active_load() 54 | self.max_sessions = max(self.max_sessions, self.last_sessions) 55 | self.max_load = max(self.max_load, self.last_load) 56 | if self.get_period_idle() == "IDLE": 57 | self.idle_duration += self.interval 58 | else: 59 | self.idle_duration = 0 60 | 61 | def get_state(self, sessions, load): 62 | if sessions > 0 and load > self.load_cutoff: 63 | return "ACTIVE" 64 | elif sessions > 0: 65 | return "SESSION" 66 | elif load > self.load_cutoff: 67 | return "LOAD" 68 | else: 69 | return "IDLE" 70 | 71 | def get_period_idle(self): 72 | return self.get_state(self.max_sessions, self.max_load) 73 | 74 | def get_last_idle(self): 75 | return self.get_state(self.last_sessions, self.last_load) 76 | 77 | def get_stats(self): 78 | return {"State": self.get_period_idle(), 79 | "LastState": self.get_last_idle(), 80 | "MaxSessions": self.max_sessions, 81 | "LastSessions": self.last_sessions, 82 | "MaxLoad": self.max_load, 83 | "LastLoad": self.last_load, 84 | "IdleDuration": self.idle_duration } 85 | 86 | -------------------------------------------------------------------------------- /ecofreq/providers/manager.py: -------------------------------------------------------------------------------- 1 | from ecofreq.providers.common import * 2 | from ecofreq.providers.mqtt import * 3 | from ecofreq.providers.rest import * 4 | 5 | class EcoProviderManager(object): 6 | PROV_DICT = {"co2signal" : CO2Signal, 7 | "electricitymaps" : ElectricityMapsProvider, 8 | "ukgrid": UKGridProvider, 9 | "watttime": WattTimeProvider, 10 | "stromgedacht": StromGedachtProvider, 11 | "energycharts": EnergyChartsProvider, 12 | "gridstatus.io": GridStatusIOProvider, 13 | "tibber": TibberProvider, 14 | "octopus": OctopusProvider, 15 | "awattar": AwattarProvider, 16 | "mqtt": MQTTEcoProvider, 17 | "mock" : MockEcoProvider, 18 | "const": ConstantProvider } 19 | 20 | def __init__(self, config): 21 | self.init_prov_dict() 22 | self.providers = {} 23 | self.set_config(config) 24 | 25 | def init_prov_dict(self): 26 | self.prov_dict = {} 27 | # TODO dynamic discovery 28 | self.prov_dict = EcoProviderManager.PROV_DICT 29 | 30 | def info_string(self): 31 | if self.providers: 32 | s = [m + " = " + p.info_string() for m, p in self.providers.items()] 33 | return ", ".join(s) 34 | else: 35 | return "None" 36 | 37 | def set_config(self, config): 38 | self.interval = int(config["provider"]["interval"]) 39 | for metric in ["all", EcoProvider.FIELD_CO2, EcoProvider.FIELD_PRICE, EcoProvider.FIELD_INDEX, EcoProvider.FIELD_FOSSIL_PCT]: 40 | if metric in config["provider"]: 41 | p = config["provider"].get(metric) 42 | if p in [None, "", "none", "off"]: 43 | self.providers.pop(metric, None) 44 | elif p.startswith("const:"): 45 | cfg = { metric: p.strip("const:") } 46 | self.providers[metric] = ConstantProvider(cfg, self.interval) 47 | elif p.startswith("mqtt"): 48 | cfg = config[p] 49 | self.providers[metric] = MQTTEcoProvider(cfg, self.interval, p) 50 | # elif p in self.PROV_DICT: 51 | elif p in self.prov_dict: 52 | try: 53 | cfg = config[p] 54 | except KeyError: 55 | cfg = {} 56 | self.providers[metric] = self.prov_dict[p](cfg, self.interval) 57 | else: 58 | raise ValueError("Unknown emission provider: " + p) 59 | 60 | def get_config(self, config={}): 61 | config["provider"] = {} 62 | config["provider"]["interval"] = self.interval 63 | for metric in self.providers: 64 | p = self.providers[metric] 65 | config["provider"][metric] = p.cfg_string() 66 | config[p.LABEL] = p.get_config() 67 | return config 68 | 69 | def get_data(self): 70 | data = {} 71 | if "all" in self.providers: 72 | data = self.providers["all"].get_data() 73 | for metric in self.providers.keys(): 74 | if metric != "all": 75 | data[metric] = self.providers[metric].get_field(metric) 76 | return data 77 | 78 | -------------------------------------------------------------------------------- /ecofreq/monitors/manager.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | 3 | from ecofreq.monitors.energy import EnergyMonitor 4 | from ecofreq.monitors.freq import FreqMonitor, CPUFreqMonitor 5 | from ecofreq.monitors.idle import IdleMonitor 6 | from ecofreq.helpers import CpuFreqHelper 7 | 8 | class MonitorManager(object): 9 | def __init__(self, config): 10 | self.monitors = EnergyMonitor.from_config(config) 11 | self.monitors += FreqMonitor.from_config(config) 12 | self.monitors += IdleMonitor.from_config(config) 13 | 14 | def info_string(self): 15 | s = [] 16 | for m in self.monitors: 17 | s.append(type(m).__name__ + " (interval = " + str(m.interval) + " sec)") 18 | return ", ".join(s) 19 | 20 | def adjust_interval(self, period): 21 | min_interval = min([m.interval for m in self.monitors]) 22 | int_ratio = ceil(period / min_interval) 23 | sample_interval = round(period / int_ratio) 24 | for m in self.monitors: 25 | if m.interval % sample_interval > 0: 26 | m.interval = sample_interval * int(m.interval / sample_interval) 27 | return sample_interval 28 | 29 | def update(self, duration): 30 | for m in self.monitors: 31 | if duration % m.interval == 0: 32 | m.update() 33 | 34 | def reset_period(self): 35 | for m in self.monitors: 36 | m.reset_period() 37 | 38 | def get_reading(self, metric, domain, method): 39 | for m in self.monitors: 40 | pass 41 | 42 | def get_period_energy(self): 43 | result = 0 44 | for m in self.monitors: 45 | if issubclass(type(m), EnergyMonitor): 46 | result += m.get_period_energy() 47 | return result 48 | 49 | def get_total_energy(self): 50 | result = 0 51 | for m in self.monitors: 52 | if issubclass(type(m), EnergyMonitor): 53 | result += m.get_total_energy() 54 | return result 55 | 56 | def get_period_avg_power(self): 57 | result = 0 58 | for m in self.monitors: 59 | if issubclass(type(m), EnergyMonitor): 60 | result += m.get_period_avg_power() 61 | return result 62 | 63 | def get_last_avg_power(self): 64 | result = 0 65 | for m in self.monitors: 66 | if issubclass(type(m), EnergyMonitor): 67 | result += m.get_last_avg_power() 68 | return result 69 | 70 | def get_last_cpu_avg_freq(self, unit=CpuFreqHelper.MHZ): 71 | for m in self.monitors: 72 | if issubclass(type(m), CPUFreqMonitor): 73 | return m.get_last_avg_freq(unit) 74 | return None 75 | 76 | def get_period_cpu_avg_freq(self, unit): 77 | for m in self.monitors: 78 | if issubclass(type(m), CPUFreqMonitor): 79 | return m.get_period_avg_freq(unit) 80 | return None 81 | 82 | def get_period_idle(self): 83 | for m in self.monitors: 84 | if issubclass(type(m), IdleMonitor): 85 | return m.get_period_idle() 86 | return None 87 | 88 | def get_by_class(self, cls): 89 | for m in self.monitors: 90 | if issubclass(type(m), cls): 91 | return m 92 | return None 93 | 94 | def get_stats(self): 95 | stats = {} 96 | for m in self.monitors: 97 | stats.update(m.get_stats()) 98 | return stats 99 | -------------------------------------------------------------------------------- /ecofreq/policy/gpu.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from inspect import isclass 3 | 4 | from ecofreq.helpers.nvidia import NvidiaGPUHelper 5 | from ecofreq.policy.common import EcoPolicy 6 | from ecofreq.config import OPTION_DISABLED 7 | 8 | class GPUEcoPolicy(EcoPolicy): 9 | def __init__(self, config): 10 | EcoPolicy.__init__(self, config) 11 | 12 | @classmethod 13 | def from_config(cls, config): 14 | if not config: 15 | return None 16 | 17 | # first, check if we have a specific EcoPolicy class 18 | c = config["control"] 19 | if c in globals(): 20 | cls = globals()[c] 21 | if isclass(cls) and issubclass(cls, GPUEcoPolicy): 22 | return cls(config) 23 | 24 | # disable GPU policy if there are no GPUs 25 | if not NvidiaGPUHelper.available(): 26 | return None 27 | 28 | # otherwise, look for a generic policy type 29 | c = c.lower() 30 | if c == "auto": 31 | if NvidiaGPUHelper.available(): 32 | c = "power" 33 | # elif CpuFreqHelper.available(): 34 | # c = "frequency" 35 | else: 36 | return None 37 | 38 | if c == "power": 39 | return GPUPowerEcoPolicy(config) 40 | elif c == "frequency": 41 | return GPUFreqEcoPolicy(config) 42 | elif c == "cgroup": 43 | # cgroup currently does not support GPUs, so let's rely on CPU scaling 44 | return None 45 | elif c in OPTION_DISABLED: 46 | return None 47 | else: 48 | raise ValueError("Unknown policy: " + c) 49 | 50 | class GPUPowerEcoPolicy(GPUEcoPolicy): 51 | UNIT={"W": 1} 52 | 53 | def __init__(self, config): 54 | GPUEcoPolicy.__init__(self, config) 55 | 56 | if not NvidiaGPUHelper.available(): 57 | print ("ERROR: NVIDIA driver not found!") 58 | sys.exit(-1) 59 | 60 | plinfo = NvidiaGPUHelper.get_power_limit_all() 61 | self.pmin = float(plinfo[0][0]) 62 | self.pmax = float(plinfo[0][1]) 63 | self.pstart = float(plinfo[0][2]) 64 | self.init_governor(config, self.pmin, self.pmax, 0) 65 | 66 | def set_power(self, power_w): 67 | if power_w and not self.debug: 68 | NvidiaGPUHelper.set_power_limit(power_w) 69 | 70 | def set_co2(self, co2): 71 | self.power = self.co2val(co2) 72 | # print("Update policy co2 -> power: ", co2, "->", self.power) 73 | self.set_power(self.power) 74 | 75 | def reset(self): 76 | self.set_power(self.pmax) 77 | 78 | class GPUFreqEcoPolicy(GPUEcoPolicy): 79 | UNIT={"MHz": 1} 80 | 81 | def __init__(self, config): 82 | GPUEcoPolicy.__init__(self, config) 83 | 84 | if not NvidiaGPUHelper.available(): 85 | print ("ERROR: NVIDIA driver not found!") 86 | sys.exit(-1) 87 | 88 | self.fmax = NvidiaGPUHelper.get_hw_max_freq()[0] 89 | self.fmin = self.fmax * 0.3 90 | self.init_governor(config, self.fmin, self.fmax, 0) 91 | 92 | def set_freq(self, freq): 93 | if freq and not self.debug: 94 | NvidiaGPUHelper.set_freq_limit(freq) 95 | 96 | def set_co2(self, co2): 97 | self.freq = self.co2val(co2) 98 | self.set_freq(self.freq) 99 | 100 | def reset(self): 101 | NvidiaGPUHelper.reset_freq_limit() 102 | -------------------------------------------------------------------------------- /doc/USAGE.md: -------------------------------------------------------------------------------- 1 | # EcoFreq usage 2 | 3 | ## ecoctl 4 | 5 | * Show EcoFreq status 6 | 7 | ``` 8 | ecoctl 9 | ``` 10 | ``` 11 | EcoFreq is RUNNING 12 | 13 | = CONFIGURATION = 14 | Log file: /var/log/ecofreq.log 15 | CO2 Provider: all = ElectricityMapsProvider (interval = 15 sec), price = EnergyChartsProvider (interval = 15 sec) 16 | CO2 Policy: CPUPowerEcoPolicy (governor = const:15.0W), metric = co2 17 | Idle Policy: None 18 | Monitors: PowercapEnergyMonitor (interval = 5 sec), CPUFreqMonitor (interval = 5 sec), IdleMonitor (interval = 5 sec) 19 | 20 | = STATUS = 21 | State: ACTIVE 22 | Load: 2.24 23 | Power [W]: 5 24 | CO2 intensity [g/kWh]: 119 25 | Energy price [ct/kWh]: 7.572 26 | 27 | = STATISTICS = 28 | Running since: 2024-05-10T21:38:06 (up 0:20:01) 29 | Energy consumed [kWh]: 0.004 30 | CO2 total [kg]: 0.000408 31 | Cost total [EUR]: 0.00027 32 | ``` 33 | 34 | * Show current policy 35 | 36 | ``` 37 | ecoctl policy 38 | ``` 39 | ``` 40 | CO2 policy: CPUPowerEcoPolicy(governor = step:100=10.5W:200=7.5W, metric = co2) 41 | 42 | CO2-aware power scaling is now ENABLED 43 | ``` 44 | 45 | * Set new policy 46 | 47 | ``` 48 | ecoctl policy const:80% 49 | ``` 50 | ``` 51 | Old policy: CPUPowerEcoPolicy(governor = step:100=10.5W:200=7.5W, metric = co2) 52 | New policy: CPUPowerEcoPolicy(governor = const:12.0W, metric = co2) 53 | 54 | CO2-aware power scaling is now ENABLED 55 | ``` 56 | 57 | * Show current provider 58 | 59 | ``` 60 | ecoctl provider 61 | ``` 62 | ``` 63 | CO2 provider: all = electricitymaps (interval = 15 s), price = energycharts (interval = 15 s) 64 | ``` 65 | 66 | 67 | ## ecorun 68 | 69 | 70 | * Run command and report energy/CO2/cost statistics: 71 | 72 | ``` 73 | ecorun 74 | ``` 75 | 76 | ``` 77 | time_s: 10.003 78 | pwr_avg_w: 88.724 79 | energy_j: 887.5 80 | energy_kwh: 0.0 81 | co2_g: 0.098 82 | cost_ct: 0.001 83 | ``` 84 | 85 | * Run command with a non-default policy: 86 | 87 | ``` 88 | ecorun -p maxperf 89 | ``` 90 | 91 | ``` 92 | ecorun -p const:0.8 93 | ``` 94 | 95 | ``` 96 | ecorun -p cpu:const:2000MHz 97 | ``` 98 | 99 | NOTE: Currently, `ecorun` assumes single-user scenario since it measures system-wide energy consumption and changes global EcoFreq state. 100 | 101 | ## ecostat 102 | 103 | * Report energy and CO2 statistics for a local EcoFreq instance (default log file): 104 | 105 | ``` 106 | ecostat 107 | ``` 108 | ``` 109 | EcoStat v0.0.1 110 | 111 | Loading data from log file: /var/log/ecofreq.log 112 | 113 | Time interval: 2022-01-01 00:03:30 - 2022-06-30 23:53:23 114 | Monitoring active: 175 days, 20:24:55 115 | Monitoring inactive: 0:16:44 116 | CO2 intensity range [g/kWh]: 109 - 545 117 | CO2 intensity mean [g/kWh]: 341 118 | Energy consumed [J]: 4358414437.5 119 | Energy consumed [kWh]: 1210.671 120 | = electric car travel [km]: 6053 121 | Total CO2 emitted [kg]: 409.507177 122 | 123 | Idle time: 41 days, 9:43:30 124 | Idle energy [kWh]: 127.437 125 | Idle = e-car travel [km]: 637 126 | Idle CO2 [kg]: 44.531762 127 | ``` 128 | 129 | * Use a different log file: 130 | 131 | ``` 132 | ecostat -l myserver.log 133 | ``` 134 | 135 | 136 | * Limit time interval: 137 | 138 | ``` 139 | ecostat --start 2024-01-01 --end 2024-02-01 140 | ``` 141 | 142 | 143 | -------------------------------------------------------------------------------- /doc/CONFIG.md: -------------------------------------------------------------------------------- 1 | # EcoFreq config file reference 2 | 3 | ## Provider 4 | 5 | The `[provider]` section configures APIs that provide real-time carbon and price signal. 6 | Each API can provide one or more metrics, such as `co2` (gCO2/kWh), `price` (ct/kWh) or `index` (discrete "traffic light" signal). 7 | For instance, below we set constant electricity price of 30 ct/kWh, and get real-time carbon intensity from ElectricityMaps every 15 min: 8 | ``` 9 | [provider] 10 | co2=electricitymaps 11 | price=const:30 12 | interval=900 13 | ``` 14 | 15 | We can use `all=` to assign the default provider (i.e. all its metrics will be used, unless explicitely overwritten). 16 | For instance, we can use traffic light signal and renewable share from EnergyCharts, carbon intensity from ElectricityMaps, and real-time price from Tibber: 17 | ``` 18 | [provider] 19 | all=energycharts 20 | co2=electricitymaps 21 | price=tibber 22 | interval=3600 23 | ``` 24 | 25 | For details about specific providers and their settings, see [PROVIDER.md](https://github.com/amkozlov/eco-freq/blob/main/doc/PROVIDER.md) 26 | 27 | ## Policy 28 | 29 | The `[policy]` section defines how to adjust power-relevant settings based on the real-time carbon/price signal. 30 | First, we define which `metric` to use, e.g. `co2` or `price`. Then, we define `control` method, such as `power` (direct power capping), 31 | `frequency` (DVFS), or `cgroup` (utilization capping). By default (`auto`), EcoFreq will use "the best" available control method on the current system. 32 | Finally, we define the exact relationship between (input) metric value and (output) control setting using the so-called `governor`. 33 | For instance, we can use `step` governor to set 80% powercap whenever carbon intensity is above 200 gCO2/kWh, and decrease it further down to 60% above 400 gCO2/kWh: 34 | 35 | ``` 36 | [policy] 37 | Metric=co2 38 | Control=power 39 | DefaultGovernor=step:200=0.8:400=0.6 40 | Governor=default 41 | ``` 42 | 43 | On hybrid systems, we can define separate policies for CPU and GPU in `[cpu_policy]` and `[gpu_policy]` sections, respectively. 44 | 45 | For details about specific policies and governors, see [POLICY.md](https://github.com/amkozlov/eco-freq/blob/main/doc/POLICY.md) 46 | 47 | ## Monitor 48 | 49 | EcoFreq supports multiple power/energy monitoring interfaces (`RAPL`, `IPMI`, `nvidia-smi`), and usually can automatically detect which ones are available. 50 | However, you can manually enforce using a specific inteface, and set polling interval in the `[monitor]` section: 51 | 52 | ``` 53 | [monitor] 54 | PowerSensor=rapl 55 | Interval=5 56 | ``` 57 | 58 | ## Server 59 | 60 | The `[server]` section defines who can use the `ecoctl` command to change EcoFreq settings on-the-fly. It works by changing the ownership of and permissions on the IPC socket file (`/var/run/ecofreq.sock`). By default, this file is owned by `root:ecofreq` with group read/write permissions (`0660`). 61 | 62 | Allow access for members of the `staff` user group: 63 | ``` 64 | [server] 65 | filegroup=staff 66 | filemode=0o660 67 | ``` 68 | 69 | Allow access for any user (not recommended): 70 | ``` 71 | [server] 72 | filemode=0o666 73 | ``` 74 | 75 | ## Suspend-on-Idle 76 | 77 | EcoFreq can automatically detect idling and switch to the low-energy standby mode (suspend-to-RAM). 78 | For instance, to suspend after 15 min (900 s) at <10% CPU utilization, add these lines to your config file: 79 | 80 | ``` 81 | [idle] 82 | IdleMonitor=on 83 | LoadCutoff=0.10 84 | # 1 = 1min average, 2 = 5min, 3 = 15min 85 | LoadPeriod=1 86 | # Switch to low-energy standby mode (suspend) after x seconds of idling 87 | SuspendAfter=900 88 | ``` 89 | **WARNING**: Before enabling this feature, please check that [Wake-on-LAN](https://wiki.archlinux.org/title/Wake-on-LAN) is enabled. 90 | Then you can later wake up a suspended system by sending a 'magic packet' (e.g., `wakeonlan `). 91 | -------------------------------------------------------------------------------- /ecofreq/ecorun.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import time 6 | from subprocess import call 7 | 8 | from ecofreq.config import SHM_FILE, JOULES_IN_KWH 9 | from ecofreq.ipc import EcoClient 10 | 11 | def read_shm(): 12 | with open(SHM_FILE) as f: 13 | ts, joules, co2, cost = [float(x) for x in f.readline().split(" ")] 14 | return ts, joules, co2, cost 15 | 16 | def set_governor(gov): 17 | try: 18 | ec = EcoClient() 19 | policy = ec.get_policy() 20 | domain = "cpu" 21 | if gov.startswith("cpu:") or gov.startswith("gpu:"): 22 | domain, gov = gov.split(":", 1) 23 | if not domain in policy["co2policy"]: 24 | policy["co2policy"][domain] = {} 25 | 26 | old_gov = policy["co2policy"][domain]["governor"] 27 | if gov.startswith("co2:") or gov.startswith("price:") or gov.startswith("fossil_pct:") or gov.startswith("index:"): 28 | old_gov = ":".join([policy["co2policy"][domain]["metric"], old_gov]) 29 | metric, gov = gov.split(":", 1) 30 | policy["co2policy"][domain]["metric"] = metric 31 | policy["co2policy"][domain]["governor"] = gov 32 | ret = ec.set_policy(policy) 33 | return old_gov 34 | except ConnectionRefusedError: 35 | print("ERROR: Connection refused! Please check that EcoFreq daemon is running.") 36 | 37 | def main(): 38 | if not os.path.exists(SHM_FILE): 39 | print("ERROR: File not found:", SHM_FILE) 40 | print("Please make sure that EcoFreq service is active!") 41 | sys.exit(-1) 42 | 43 | # print(sys.argv) 44 | 45 | outfile = None 46 | runname = "noname" 47 | iters = 1 48 | cmdline_start = 1 49 | if len(sys.argv) > 3 and sys.argv[1] == "-p": 50 | gov = sys.argv[2] 51 | if gov in ["off", "disabled"]: 52 | gov = "none" 53 | elif gov in ["on", "enabled", "default", "eco"]: 54 | gov = "default" 55 | old_gov = set_governor(gov) 56 | cmdline_start += 2 57 | else: 58 | old_gov = None 59 | 60 | if len(sys.argv) > 5 and sys.argv[3] == "-o": 61 | outfile = sys.argv[4] 62 | cmdline_start += 2 63 | 64 | if len(sys.argv) > 7 and sys.argv[5] == "-n": 65 | runname = sys.argv[6] 66 | cmdline_start += 2 67 | 68 | if len(sys.argv) > 9 and sys.argv[7] == "-i": 69 | iters = int(sys.argv[8]) 70 | cmdline_start += 2 71 | 72 | cmdline = sys.argv[cmdline_start:] 73 | 74 | start_ts, start_joules, start_co2, start_cost = read_shm() 75 | 76 | start_time = time.time() 77 | 78 | # call(cmdline, shell=True) 79 | for i in range(iters): 80 | call(cmdline) 81 | 82 | end_time = time.time() 83 | 84 | start_ts, end_joules, end_co2, end_cost = read_shm() 85 | 86 | if old_gov: 87 | set_governor(old_gov) 88 | 89 | diff_joules = end_joules - start_joules 90 | diff_kwh = diff_joules / JOULES_IN_KWH 91 | diff_co2 = end_co2 - start_co2 92 | diff_cost = end_cost - start_cost 93 | 94 | diff_time = end_time - start_time 95 | avg_pwr = diff_joules / diff_time 96 | 97 | print("") 98 | print("time_s: ", round(diff_time, 3)) 99 | print("pwr_avg_w: ", round(avg_pwr, 3)) 100 | print("energy_j: ", round(diff_joules, 3)) 101 | print("energy_kwh:", round(diff_kwh, 3)) 102 | print("co2_g: ", round(diff_co2, 3)) 103 | print("cost_ct: ", round(diff_cost, 3)) 104 | 105 | if outfile: 106 | if not os.path.exists(outfile): 107 | with open(outfile, "w") as f: 108 | headers = ["name", "policy", "time_s", "pwr_avg_w", "energy_j", "energy_kwh", "co2_g", "cost_ct"] 109 | f.write(",".join(headers) + "\n"); 110 | 111 | with open(outfile, "a") as f: 112 | vals = [runname, gov] 113 | res = [diff_time, avg_pwr, diff_joules, diff_kwh, diff_co2, diff_cost] 114 | vals += [str(round(x, 3)) for x in res] 115 | f.write(",".join(vals) + "\n") 116 | 117 | 118 | if __name__ == '__main__': 119 | main() 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EcoFreq: compute with cleaner & cheaper energy 2 | 3 | [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] 4 | 5 | [cc-by-nc-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/ 6 | [cc-by-nc-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-blue.svg 7 | 8 | In many regions with a high share of renewables - such as Germany, Spain, UK or California - CO2 emissions per kWh of electricity may vary two-fold within a single day, and up to four-fold within a year. This is due to both, variable production from solar and wind, and variable demand (peak hours vs. night-time/weekends). Hence, reducing energy consumption during these periods of high carbon intesity leads to overproportionate CO2 savings. This is exactly the idea behind EcoFreq: it modulates CPU/GPU power consumption *in realtime* according to the current "greenness" of the grid energy mix. Importantly, this modulation is absolutely transparent to user applications: they will run as usual without interruption, "accelerating" in times when energy comes mostly from renewables, and being throttled when fossil generation increases. 9 | 10 | And it gets even better if you have dynamic electricity pricing (e.g., [Octopus](https://octopus.energy/smart/agile/), [Tibber](https://tibber.com/en) etc.) or on-site solar panels: (being an) EcoFreq can save you a few cents ;) 11 | 12 | **TL;DR Compute faster when electricity is abundant and green, throttle down when it is expensive and dirty:** 13 | ![](https://github.com/amkozlov/eco-freq/blob/dev/img/ecofreq_tldr.png?raw=true) 14 | 15 | ## Installation 16 | 17 | Prerequisites: 18 | - Linux system (tested with Ubuntu and CentOS) 19 | - Python3.8+ with `pip` (`pipx` recommended) 20 | - (optional) API token -> [Which real-time CO2/price provider to use?](https://github.com/amkozlov/eco-freq/blob/main/config/README.md/) 21 | - (optional) [`ipmitool`](https://github.com/ipmitool/ipmitool) to use IPMI power measurements 22 | 23 | First, install EcoFreq package using `pip` or `pipx`: 24 | 25 | ``` 26 | pipx install ecofreq 27 | ``` 28 | 29 | This will install EcoFreq locally under the current (non-privileged) user. Since power limiting typically requires root privileges, you will need sudo permissions to use all features of EcoFreq ([see details](https://github.com/amkozlov/eco-freq/blob/dev/doc/INSTALL.md#Permissions)). 30 | 31 | Alternatively, you can install and run EcoFreq under root: 32 | ``` 33 | sudo pipx install ecofreq 34 | ``` 35 | This is less secure, but makes configuration simpler. 36 | 37 | For production use, you will likely want to configure EcoFreq daemon to run in the background: ([HOWTO](https://github.com/amkozlov/eco-freq/blob/dev/doc/INSTALL.md#Daemon)) 38 | 39 | ## Usage 40 | 41 | * Show information about your system and its power limiting capabilities: 42 | 43 | ``` 44 | ecofreq info 45 | ``` 46 | 47 | * For a quick test of EcoFreq on your system without configuration overhead (using mock CO2 provider): 48 | 49 | ``` 50 | ecofreq -c mock -l test.log 51 | ``` 52 | 53 | * After [installing EcoFreq as a service](https://github.com/amkozlov/eco-freq/blob/dev/doc/INSTALL.md#Daemon), you can use standard `systemctl` commands to control it. 54 | 55 | ``` 56 | sudo systemctl start ecofreq 57 | sudo systemctl status ecofreq 58 | sudo systemctl stop ecofreq 59 | ``` 60 | 61 | Command-line tool `ecoctl` allows to query and control the EcoFreq service. 62 | If you want to run `ecoctl` without `sudo` (recommended), either add your user to the `ecofreq` group, 63 | or [configure socket permissions accordingly](https://github.com/amkozlov/eco-freq/blob/main/doc/CONFIG.md#Server). 64 | 65 | * Show EcoFreq status: 66 | 67 | ``` 68 | ecoctl 69 | ``` 70 | 71 | * Change power scaling policy: 72 | 73 | ``` 74 | ecoctl policy co2:step:100=0.7:200=0.5 75 | 76 | ecoctl policy const:50% 77 | 78 | ecoctl policy maxperf 79 | ``` 80 | 81 | * Report energy and CO2 for a program run (assuming it runs exclusively -> to be improved): 82 | 83 | ``` 84 | ecorun sleep 10 85 | 86 | time_s: 10.003 87 | pwr_avg_w: 88.724 88 | energy_j: 887.5 89 | energy_kwh: 0.0 90 | co2_g: 0.098 91 | cost_ct: 0.001 92 | ``` 93 | 94 | * Report energy and CO2 statistics for a local EcoFreq instance (default log file): 95 | 96 | ``` 97 | ecostat.py 98 | 99 | EcoStat v0.0.1 100 | 101 | Loading data from log file: /var/log/ecofreq.log 102 | 103 | Time interval: 2022-01-01 00:03:30 - 2022-06-30 23:53:23 104 | Monitoring active: 175 days, 20:24:55 105 | Monitoring inactive: 0:16:44 106 | CO2 intensity range [g/kWh]: 109 - 545 107 | CO2 intensity mean [g/kWh]: 341 108 | Energy consumed [J]: 4358414437.5 109 | Energy consumed [kWh]: 1210.671 110 | = electric car travel [km]: 6053 111 | Total CO2 emitted [kg]: 409.507177 112 | 113 | Idle time: 41 days, 9:43:30 114 | Idle energy [kWh]: 127.437 115 | Idle = e-car travel [km]: 637 116 | Idle CO2 [kg]: 44.531762 117 | ``` 118 | 119 | For more examples, see [USAGE.md](https://github.com/amkozlov/eco-freq/blob/main/doc/USAGE.md/) 120 | 121 | ## Configuration 122 | 123 | See [CONFIG.md](https://github.com/amkozlov/eco-freq/blob/main/doc/CONFIG.md/) 124 | 125 | ## Citation 126 | 127 | Oleksiy Kozlov, Alexandros Stamatakis. **EcoFreq: Compute with Cheaper, Cleaner Energy via Carbon-Aware Power Scaling**, *ISC-2024*. 128 | 129 | Open access: https://ieeexplore.ieee.org/document/10528928 130 | -------------------------------------------------------------------------------- /ecofreq/policy/governor.py: -------------------------------------------------------------------------------- 1 | from ecofreq.config import OPTION_DISABLED 2 | 3 | class Governor(object): 4 | LABEL="None" 5 | 6 | def __init__(self, args, vmin, vmax): 7 | self.val_round = 3 8 | 9 | def info_args(self): 10 | return {} 11 | 12 | def info_string(self, unit={"": 1}): 13 | args = [self.LABEL] 14 | d = self.info_args() 15 | uname, ufactor = list(unit.items())[0] 16 | for k in d.keys(): 17 | if d[k]: 18 | arg = "{0}={1}{2}".format(k, d[k] / ufactor, uname) 19 | else: 20 | arg = "{0}{1}".format(k / ufactor, uname) 21 | args.append(arg) 22 | return ":".join(args) 23 | 24 | def round_val(self, val): 25 | return int(round(val, self.val_round)) 26 | 27 | @classmethod 28 | def parse_args(cls, toks): 29 | args = {} 30 | for t in toks: 31 | if "=" in t: 32 | k, v = t.split("=") 33 | args[k] = v 34 | else: 35 | args[t] = None 36 | return args 37 | 38 | @classmethod 39 | def parse_val(cls, vstr, vmin, vmax, units={}): 40 | if vstr == "min": 41 | val = vmin 42 | elif vstr == "max": 43 | val = vmax 44 | else: 45 | val = None 46 | # absolute value with unit specifier (W, MHz etc.) 47 | for uname, ufactor in units.items(): 48 | if vstr.endswith(uname.lower()): 49 | val = float(vstr.strip(uname.lower())) * ufactor 50 | break 51 | if not val: 52 | # relative value 53 | if vstr.endswith("%"): 54 | p = float(vstr.strip("%")) / 100 55 | else: 56 | p = float(vstr) 57 | val = vmax * p 58 | if val > vmax or val < vmin: 59 | raise ValueError("Constant governor parameter out-of-bounds: " + vstr) 60 | return val 61 | 62 | @classmethod 63 | def from_config(cls, config, vmin, vmax, units): 64 | govstr = config["governor"].lower() 65 | if govstr == "default": 66 | govstr = config["defaultgovernor"].lower() 67 | toks = govstr.split(":") 68 | t = toks[0] 69 | args = cls.parse_args(toks[1:]) 70 | if t == "linear" or t == "lineargovernor": 71 | return LinearGovernor(args, vmin, vmax, units) 72 | elif t == "step": 73 | return StepGovernor(args, vmin, vmax, units) 74 | elif t == "list": 75 | return ListGovernor(args, vmin, vmax, units) 76 | elif t == "maxperf": 77 | args = {} 78 | return ConstantGovernor(args, vmin, vmax, units) 79 | elif t == "const": 80 | return ConstantGovernor(args, vmin, vmax, units) 81 | elif t in OPTION_DISABLED: 82 | return None 83 | else: 84 | raise ValueError("Unknown governor: " + t) 85 | 86 | class ConstantGovernor(Governor): 87 | LABEL="const" 88 | 89 | def __init__(self, args, vmin, vmax, units): 90 | Governor.__init__(self, args, vmin, vmax) 91 | if len(args) > 0: 92 | s = list(args.keys())[0] 93 | self.val = Governor.parse_val(s, vmin, vmax, units) 94 | else: 95 | self.val = vmax 96 | 97 | def info_args(self): 98 | return {self.round_val(self.val) : None} 99 | 100 | def co2val(self, co2): 101 | return round(self.val, self.val_round) 102 | 103 | class LinearGovernor(Governor): 104 | LABEL="linear" 105 | 106 | def __init__(self, args, vmin, vmax, units): 107 | Governor.__init__(self, args, vmin, vmax) 108 | self.co2min = self.co2max = -1 109 | self.vmin = vmin 110 | self.vmax = vmax 111 | if len(args) == 2: 112 | self.co2min, self.co2max = sorted([int(x) for x in args.keys()]) 113 | v1 = args[str(self.co2max)] 114 | v2 = args[str(self.co2min)] 115 | if v1: 116 | self.vmin = Governor.parse_val(v1, vmin, vmax, units) 117 | if v2: 118 | self.vmax = Governor.parse_val(v2, vmin, vmax, units) 119 | 120 | def info_args(self): 121 | args = {} 122 | args[self.co2min] = self.round_val(self.vmax) 123 | args[self.co2max] = self.round_val(self.vmin) 124 | return args 125 | 126 | def co2val(self, co2): 127 | co2 = float(co2) 128 | if co2 >= self.co2max: 129 | k = 0.0 130 | elif co2 <= self.co2min: 131 | k = 1.0 132 | else: 133 | k = 1.0 - float(co2 - self.co2min) / (self.co2max - self.co2min) 134 | val = self.vmin + (self.vmax - self.vmin) * k 135 | val = int(round(val, self.val_round)) 136 | return val 137 | 138 | class StepGovernor(Governor): 139 | LABEL="step" 140 | 141 | def __init__(self, args, vmin, vmax, units, discrete=False): 142 | Governor.__init__(self, args, vmin, vmax) 143 | self.vmin = vmin 144 | self.vmax = vmax 145 | self.discrete = discrete 146 | self.steps = [] 147 | if self.discrete: 148 | klist = args.keys() 149 | else: 150 | klist = sorted([int(x) for x in args.keys()], reverse=True) 151 | for s in klist: 152 | v = Governor.parse_val(args[str(s)], vmin, vmax, units) 153 | self.steps.append((s, v)) 154 | 155 | def info_args(self): 156 | args = {} 157 | for s, v in reversed(self.steps): 158 | args[s] = v 159 | return args 160 | 161 | def co2val(self, co2): 162 | val = self.vmax 163 | for s, v in self.steps: 164 | if (self.discrete and str(co2) == s) or (not self.discrete and float(co2) >= s): 165 | val = v 166 | break 167 | val = int(round(val, self.val_round)) 168 | return val 169 | 170 | class ListGovernor(StepGovernor): 171 | LABEL="list" 172 | 173 | def __init__(self, args, vmin, vmax, units): 174 | StepGovernor.__init__(self, args, vmin, vmax, units, True) 175 | -------------------------------------------------------------------------------- /ecofreq/helpers/cgroup.py: -------------------------------------------------------------------------------- 1 | from ecofreq.utils import * 2 | 3 | class LinuxCgroupHelper(object): 4 | CGROUP_FS_PATH="/sys/fs/cgroup/" 5 | 6 | @classmethod 7 | def available(cls): 8 | return os.path.exists(cls.CGROUP_FS_PATH) 9 | 10 | @classmethod 11 | def subsystems(cls, grp=""): 12 | sub = [] 13 | if cls.available(): 14 | for sname in ["cpu", "freezer"]: 15 | if cls.enabled(sname, grp): 16 | sub.append(sname) 17 | return sub 18 | 19 | @classmethod 20 | def info(cls): 21 | print("Linux cgroup available: ", end ="") 22 | if cls.available(): 23 | print("YES", end ="") 24 | helper = None 25 | if LinuxCgroupV1Helper.enabled(): 26 | helper = LinuxCgroupV1Helper 27 | elif LinuxCgroupV2Helper.enabled(): 28 | helper = LinuxCgroupV2Helper 29 | 30 | if helper: 31 | print(" ({}) ({})".format(helper.VERSION, ",".join(helper.subsystems()))) 32 | else: 33 | print("(disabled)") 34 | else: 35 | print("NO") 36 | 37 | class LinuxCgroupV1Helper(LinuxCgroupHelper): 38 | VERSION = "v1" 39 | PROCS_FILE="cgroup.procs" 40 | CFS_QUOTA_FILE="cpu.cfs_quota_us" 41 | CFS_PERIOD_FILE="cpu.cfs_period_us" 42 | FREEZER_STATE_FILE="freezer.state" 43 | 44 | @classmethod 45 | def subsys_path(cls, sub): 46 | return os.path.join(cls.CGROUP_FS_PATH, sub) 47 | 48 | @classmethod 49 | def subsys_file(cls, sub, grp, fname): 50 | return os.path.join(cls.subsys_path(sub), grp, fname) 51 | 52 | @classmethod 53 | def procs_file(cls, sub, grp): 54 | return cls.subsys_file(sub, grp, cls.PROCS_FILE) 55 | 56 | @classmethod 57 | def cfs_quota_file(cls, grp): 58 | return cls.subsys_file("cpu", grp, cls.CFS_QUOTA_FILE) 59 | 60 | @classmethod 61 | def cfs_period_file(cls, grp): 62 | return cls.subsys_file("cpu", grp, cls.CFS_PERIOD_FILE) 63 | 64 | @classmethod 65 | def freezer_state_file(cls, grp): 66 | return cls.subsys_file("freezer", grp, cls.FREEZER_STATE_FILE) 67 | 68 | @classmethod 69 | def read_cgroup_int(cls, sub, grp, fname): 70 | return read_int_value(cls.subsys_file(sub, grp, fname)) 71 | 72 | @classmethod 73 | def get_cpu_cfs_period_us(cls, grp): 74 | return read_int_value(cls.cfs_period_file(grp)) 75 | 76 | @classmethod 77 | def set_cpu_cfs_period_us(cls, grp, period_us): 78 | return write_value(cls.cfs_period_file(grp), period_us) 79 | 80 | @classmethod 81 | def get_cpu_cfs_quota_us(cls, grp): 82 | return read_int_value(cls.cfs_quota_file(grp)) 83 | 84 | @classmethod 85 | def set_cpu_cfs_quota_us(cls, grp, quota_us): 86 | write_value(cls.cfs_quota_file(grp), int(quota_us)) 87 | 88 | @classmethod 89 | def set_cpu_quota(cls, grp, quota, period=None): 90 | if period: 91 | cls.set_cpu_cfs_period_us(grp, period) 92 | else: 93 | period = cls.get_cpu_cfs_period_us(grp) 94 | quota_us = int(quota * period) 95 | cls.set_cpu_cfs_quota_us(grp, quota_us) 96 | 97 | @classmethod 98 | def get_cpu_quota(cls, grp, ncores): 99 | quota_us = cls.get_cpu_cfs_quota_us(grp) 100 | period_us = cls.get_cpu_cfs_period_us(grp) 101 | if quota_us == -1: 102 | return ncores 103 | else: 104 | return float(quota_us) / period_us 105 | 106 | @classmethod 107 | def freeze(cls, grp): 108 | write_value(cls.freezer_state_file(grp), "FROZEN") 109 | 110 | @classmethod 111 | def unfreeze(cls, grp): 112 | write_value(cls.freezer_state_file(grp), "THAWED") 113 | 114 | @classmethod 115 | def add_proc_to_cgroup(cls, grp, pid): 116 | write_value(cls.procs_file(grp), pid) 117 | 118 | @classmethod 119 | def enabled(cls, sub="cpu", grp=""): 120 | return os.path.isfile(cls.procs_file(sub, grp)) 121 | 122 | class LinuxCgroupV2Helper(LinuxCgroupHelper): 123 | VERSION = "v2" 124 | PROCS_FILE="cgroup.procs" 125 | CPU_QUOTA_FILE="cpu.max" 126 | FREEZER_STATE_FILE="cgroup.freeze" 127 | 128 | @classmethod 129 | def subsys_file(cls, grp, fname): 130 | return os.path.join(cls.CGROUP_FS_PATH, grp, fname) 131 | 132 | @classmethod 133 | def procs_file(cls, grp): 134 | return cls.subsys_file(grp, cls.PROCS_FILE) 135 | 136 | @classmethod 137 | def freezer_state_file(cls, grp): 138 | return cls.subsys_file(grp, cls.FREEZER_STATE_FILE) 139 | 140 | @classmethod 141 | def cpu_quota_file(cls, grp): 142 | return cls.subsys_file(grp, cls.CPU_QUOTA_FILE) 143 | 144 | @classmethod 145 | def freeze(cls, grp): 146 | write_value(cls.freezer_state_file(grp), "1") 147 | 148 | @classmethod 149 | def unfreeze(cls, grp): 150 | write_value(cls.freezer_state_file(grp), "0") 151 | 152 | @classmethod 153 | def set_cpu_quota(cls, grp, quota, period=None): 154 | fname = cls.cpu_quota_file(grp) 155 | old_period = read_value(fname, 1) 156 | if not period: 157 | period = old_period 158 | if not isinstance(quota, str): 159 | quota = quota * int(period) 160 | # print(quota, period) 161 | write_value(fname, quota) 162 | 163 | @classmethod 164 | def get_cpu_quota(cls, grp, ncores): 165 | fname = cls.cpu_quota_file(grp) 166 | quota, period = read_value(fname).split(" ") 167 | if quota == "max": 168 | return ncores 169 | else: 170 | return float(quota) / int(period) 171 | 172 | @classmethod 173 | def enabled(cls, sub="", grp=""): 174 | if not sub: 175 | return os.path.isfile(cls.procs_file(grp)) 176 | elif sub == "cpu": 177 | return os.path.isfile(cls.cpu_quota_file(grp)) 178 | elif sub == "freezer": 179 | return os.path.isfile(cls.freezer_state_file(grp)) 180 | else: 181 | return False 182 | -------------------------------------------------------------------------------- /ecofreq/helpers/amd.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | from subprocess import check_output,DEVNULL,CalledProcessError 4 | 5 | from ecofreq.utils import read_int_value 6 | from .cpu import CpuInfoHelper 7 | 8 | class AMDEsmiHelper(object): 9 | CMD_ESMI_TOOL="/opt/e-sms/e_smi/bin/e_smi_tool" 10 | MAX_PLIMIT_LABEL="PowerLimitMax (Watts)" 11 | CUR_PLIMIT_LABEL="PowerLimit (Watts)" 12 | UWATT, MWATT, WATT = 1e-6, 1e-3, 1 13 | 14 | @classmethod 15 | def run_esmi(cls, params, parse_out=True): 16 | cmdline = cls.CMD_ESMI_TOOL + " " + params 17 | try: 18 | out = check_output(cmdline, shell=True, stderr=DEVNULL, universal_newlines=True) 19 | except CalledProcessError as e: 20 | if e.returncode == 210: 21 | out = e.output 22 | else: 23 | raise e 24 | if parse_out: 25 | result = {} 26 | for line in out.split("\n"): 27 | if line: 28 | toks = line.split("|") 29 | if len(toks) > 2: 30 | field = toks[1].strip() 31 | result[field] = toks[2:-1] 32 | # print(result) 33 | return result 34 | 35 | @classmethod 36 | def available(cls): 37 | try: 38 | out = cls.run_esmi("-v") 39 | return True 40 | except CalledProcessError: 41 | return False 42 | 43 | @classmethod 44 | def enabled(cls, pkg=0): 45 | try: 46 | if cls.get_package_power_limit(pkg): 47 | return True 48 | else: 49 | return False 50 | except CalledProcessError: 51 | return False 52 | 53 | @classmethod 54 | def get_field(cls, out, field, pkg=0): 55 | if pkg >= 0: 56 | return out[field][pkg] 57 | else: 58 | return out[field] 59 | 60 | @classmethod 61 | def get_package_hw_max_power(cls, pkg, unit=WATT): 62 | if cls.available(): 63 | params = "--showsockpower" 64 | out = cls.run_esmi(params) 65 | limit_w = float(cls.get_field(out, cls.MAX_PLIMIT_LABEL, pkg)) 66 | return limit_w / unit 67 | else: 68 | return None 69 | 70 | @classmethod 71 | def get_package_power_limit(cls, pkg, unit=WATT): 72 | if cls.available(): 73 | params = "--showsockpower" 74 | out = cls.run_esmi(params) 75 | limit_w = float(cls.get_field(out, cls.CUR_PLIMIT_LABEL, pkg)) 76 | return limit_w / unit 77 | else: 78 | return None 79 | 80 | @classmethod 81 | def get_power_limit(cls, unit=WATT): 82 | if cls.available(): 83 | params = "--showsockpower" 84 | out = cls.run_esmi(params) 85 | pkg_limit_w = cls.get_field(out, cls.CUR_PLIMIT_LABEL, -1) 86 | limit_w = sum([float(x) for x in pkg_limit_w]) 87 | return limit_w / unit 88 | else: 89 | return None 90 | 91 | @classmethod 92 | def set_package_power_limit(cls, pkg, power, unit=WATT): 93 | # value must be in mW ! 94 | val = round(power * unit / cls.MWATT) 95 | params = "--setpowerlimit {:d} {:d}".format(pkg, val) 96 | cls.run_esmi(params, False) 97 | 98 | @classmethod 99 | def set_power_limit(cls, power, unit=WATT): 100 | num_sockets = CpuInfoHelper.get_sockets() 101 | for pkg in range(num_sockets): 102 | cls.set_package_power_limit(pkg, power, unit) 103 | 104 | @classmethod 105 | def info(cls): 106 | if cls.available(): 107 | outfmt = "ESMI CPU{0}: max_hw_limit = {1} W, current_limit = {2} W" 108 | params = "" 109 | out = cls.run_esmi(params) 110 | num_sockets = int(cls.get_field(out, "NR_SOCKETS")) 111 | for pkg in range(num_sockets): 112 | maxp = float(cls.get_field(out, cls.MAX_PLIMIT_LABEL, pkg)) 113 | curp = float(cls.get_field(out, cls.CUR_PLIMIT_LABEL, pkg)) 114 | print(outfmt.format(pkg, maxp, curp)) 115 | else: 116 | print("AMD E-SMI tool not found.") 117 | 118 | # Code adapted from s-tui: 119 | # https://github.com/amanusk/s-tui/commit/5c87727f5a2364697bfce84a0b688c1a6d2b3250 120 | class AMDRaplMsrHelper(object): 121 | MSR_CPU_PATH="/dev/cpu/{0}/msr" 122 | TOPOL_CPU_PATH="/sys/devices/system/cpu/cpu{0}/topology/physical_package_id" 123 | CPU_MAX = 4096 124 | UNIT_MSR = 0xC0010299 125 | CORE_MSR = 0xC001029A 126 | PACKAGE_MSR = 0xC001029B 127 | ENERGY_UNIT_MASK = 0x1F00 128 | ENERGY_STATUS_MASK = 0xffffffff 129 | UJOULE_IN_JOULE = 1e6 130 | 131 | @staticmethod 132 | def read_msr(filename, register): 133 | with open(filename, "rb") as f: 134 | f.seek(register) 135 | res = int.from_bytes(f.read(8), sys.byteorder) 136 | return res 137 | 138 | @classmethod 139 | def package_list(cls): 140 | pkg_list = set() 141 | for cpu in range(cls.CPU_MAX): 142 | fname = cls.TOPOL_CPU_PATH.format(cpu) 143 | if not os.path.isfile(fname): 144 | break 145 | pkg = read_int_value(fname) 146 | pkg_list.add(pkg) 147 | return list(pkg_list) 148 | 149 | @classmethod 150 | def pkg_to_cpu(cls, pkg): 151 | for cpu in range(cls.CPU_MAX): 152 | fname = cls.TOPOL_CPU_PATH.format(cpu) 153 | if not os.path.isfile(fname): 154 | break; 155 | if read_int_value(fname) == cpu: 156 | return cpu 157 | return None 158 | 159 | @classmethod 160 | def cpu_msr_file(cls, cpu): 161 | return cls.MSR_CPU_PATH.format(cpu) 162 | 163 | @classmethod 164 | def pkg_msr_file(cls, pkg): 165 | cpu = cls.pkg_to_cpu(pkg) 166 | return cls.cpu_msr_file(cpu) 167 | 168 | @classmethod 169 | def get_energy_factor(cls, filename): 170 | unit_msr = cls.read_msr(filename, cls.UNIT_MSR) 171 | energy_factor = 0.5 ** ((unit_msr & cls.ENERGY_UNIT_MASK) >> 8) 172 | return energy_factor * cls.UJOULE_IN_JOULE 173 | 174 | @classmethod 175 | def get_energy_range(cls, filename): 176 | return cls.ENERGY_STATUS_MASK * cls.get_energy_factor(filename) 177 | 178 | @classmethod 179 | def get_energy(cls, filename, register): 180 | energy_factor = cls.get_energy_factor(filename) 181 | package_msr = cls.read_msr(filename, register) 182 | energy = package_msr * energy_factor 183 | # print ("amd pkg_energy: ", energy) 184 | return energy 185 | 186 | @classmethod 187 | def get_package_energy(cls, pkg): 188 | filename = cls.pkg_msr_file(pkg) 189 | return cls.get_energy(filename, cls.PACKAGE_MSR) 190 | 191 | @classmethod 192 | def get_core_energy(cls, cpu): 193 | filename = cls.cpu_msr_file(cpu) 194 | return cls.get_energy(filename, cls.CORE_MSR) 195 | 196 | @classmethod 197 | def get_package_energy_range(cls, pkg): 198 | filename = cls.pkg_msr_file(pkg) 199 | return cls.get_energy_range(filename) 200 | 201 | @classmethod 202 | def get_core_energy_range(cls, cpu): 203 | filename = cls.cpu_msr_file(cpu) 204 | return cls.get_energy_range(filename) 205 | -------------------------------------------------------------------------------- /ecofreq/providers/common.py: -------------------------------------------------------------------------------- 1 | import random 2 | import os.path 3 | from _collections import deque 4 | 5 | from ecofreq.config import HOMEDIR 6 | 7 | class EcoProvider(object): 8 | LABEL=None 9 | FIELD_CO2='co2' 10 | FIELD_PRICE='price' 11 | FIELD_TAX='tax' 12 | FIELD_FOSSIL_PCT='fossil_pct' 13 | FIELD_REN_PCT='ren_pct' 14 | FIELD_INDEX='index' 15 | FIELD_DEFAULT='_default' 16 | PRICE_UNITS= {"ct/kWh": 1., 17 | "eur/kWh": 100., 18 | "eur/mwh": 0.1, 19 | } 20 | 21 | def __init__(self, config, glob_interval): 22 | if "interval" in config: 23 | self.interval = int(config["interval"]) 24 | else: 25 | self.interval = glob_interval 26 | 27 | def cfg_string(self): 28 | return self.LABEL 29 | 30 | def info_string(self): 31 | return type(self).__name__ + " (interval = " + str(self.interval) + " sec)" 32 | 33 | def get_field(self, field, data=None): 34 | if not data: 35 | data = self.get_data() 36 | try: 37 | if not field in data: 38 | field =self.FIELD_DEFAULT 39 | return float(data[field]) 40 | except: 41 | return None 42 | 43 | def get_co2(self, data=None): 44 | return self.get_field(self.FIELD_CO2, data) 45 | 46 | def get_fossil_pct(self, data=None): 47 | return self.get_field(self.FIELD_FOSSIL_PCT, data) 48 | 49 | def get_price(self, data=None): 50 | return self.get_field(self.FIELD_PRICE, data) 51 | 52 | def get_config(self): 53 | cfg = {} 54 | cfg["interval"] = self.interval 55 | return cfg 56 | 57 | class ConstantProvider(EcoProvider): 58 | LABEL="const" 59 | 60 | def __init__(self, config, glob_interval): 61 | EcoProvider.__init__(self, config, glob_interval) 62 | self.set_config(config) 63 | 64 | def info_string(self): 65 | return self.cfg_string() 66 | 67 | def cfg_string(self): 68 | return "{0}:{1}".format(self.LABEL, self.data[self.metric]) 69 | 70 | def get_config(self): 71 | cfg = super().get_config() 72 | cfg[self.metric] = self.data[self.metric] 73 | return cfg 74 | 75 | def set_config(self, config): 76 | self.data = {} 77 | for m, v in config.items(): 78 | self.metric = m 79 | self.data[m] = float(v) 80 | 81 | def get_data(self): 82 | return self.data 83 | 84 | class MockEcoProvider(EcoProvider): 85 | LABEL="mock" 86 | 87 | def __init__(self, config, glob_interval): 88 | EcoProvider.__init__(self, config, glob_interval) 89 | self.co2file = None 90 | self.set_config(config) 91 | 92 | def get_config(self): 93 | cfg = super().get_config() 94 | cfg["co2range"] = "{0}-{1}".format(self.co2min, self.co2max) 95 | cfg["co2file"] = self.co2file 96 | return cfg 97 | 98 | def set_config(self, config): 99 | co2range = '100-800' 100 | co2range = config.get('co2range', co2range) 101 | self.co2file = config.get('co2file', None) 102 | if self.co2file and not os.path.isabs(self.co2file): 103 | self.co2file = str(HOMEDIR / self.co2file) 104 | self.co2min, self.co2max = [int(x) for x in co2range.split("-")] 105 | self.read_co2_file() 106 | 107 | def read_co2_file(self): 108 | if self.co2file: 109 | if not os.path.isfile(self.co2file): 110 | raise ValueError("File not found: " + self.co2file) 111 | self.co2queue = deque() 112 | self.fossil_queue = deque() 113 | self.price_queue = deque() 114 | self.index_queue = deque() 115 | co2_field = 0 116 | fossil_field = 1 117 | index_field = -1 118 | with open(self.co2file) as f: 119 | for line in f: 120 | if line.startswith("##"): 121 | pass 122 | elif line.startswith("#"): 123 | toks = [x.strip() for x in line.replace("#", "", 1).split("\t")] 124 | try: 125 | co2_field = toks.index('CI [g/kWh]') 126 | except ValueError: 127 | co2_field = toks.index('gCO2/kWh') 128 | try: 129 | fossil_field = toks.index('Fossil [%]') 130 | except ValueError: 131 | fossil_field = -1 132 | if 'Price/kWh' in toks: 133 | price_field = toks.index('Price/kWh') 134 | price_factor = 1 135 | elif 'EUR/MWh'in toks: 136 | price_field = toks.index('EUR/MWh') 137 | price_factor = 0.1 138 | else: 139 | price_field = -1 140 | if 'co2index' in toks: 141 | index_field = toks.index('co2index') 142 | elif 'Index' in toks: 143 | index_field = toks.index('Index') 144 | else: 145 | toks = line.split("\t") 146 | co2 = None if toks[co2_field].strip() == "NA" else float(toks[co2_field]) 147 | self.co2queue.append(co2) 148 | if fossil_field >= 0 and fossil_field < len(toks): 149 | fossil_pct = None if toks[fossil_field].strip() == "NA" else float(toks[fossil_field]) 150 | self.fossil_queue.append(fossil_pct) 151 | if price_field >= 0 and price_field < len(toks): 152 | price_kwh = None if toks[price_field].strip() == "NA" else float(toks[price_field]) * price_factor 153 | self.price_queue.append(price_kwh) 154 | if index_field >= 0 and index_field < len(toks): 155 | index = None if toks[index_field].strip() == "NA" else toks[index_field].strip() 156 | self.index_queue.append(index) 157 | else: 158 | self.co2queue = None 159 | self.fossil_queue = None 160 | self.price_queue = None 161 | self.index_queue = None 162 | 163 | def get_data(self): 164 | if self.co2queue and len(self.co2queue) > 0: 165 | co2 = self.co2queue.popleft() 166 | self.co2queue.append(co2) 167 | fossil_pct = None 168 | else: 169 | co2 = random.randint(self.co2min, self.co2max) 170 | 171 | if self.fossil_queue and len(self.fossil_queue) > 0: 172 | fossil_pct = self.fossil_queue.popleft() 173 | self.fossil_queue.append(fossil_pct) 174 | elif co2: 175 | fossil_pct = (co2 - self.co2min) / (self.co2max - self.co2min) 176 | fossil_pct = min(max(fossil_pct, 0), 1) * 100 177 | 178 | if self.price_queue and len(self.price_queue) > 0: 179 | price_kwh = self.price_queue.popleft() 180 | self.price_queue.append(price_kwh) 181 | else: 182 | price_kwh = None 183 | 184 | if self.index_queue and len(self.index_queue) > 0: 185 | index = self.index_queue.popleft() 186 | self.index_queue.append(index) 187 | else: 188 | index = None 189 | 190 | data = {} 191 | data[self.FIELD_CO2] = co2 192 | data[self.FIELD_FOSSIL_PCT] = fossil_pct 193 | data[self.FIELD_PRICE] = price_kwh 194 | data[self.FIELD_INDEX] = index 195 | return data 196 | 197 | -------------------------------------------------------------------------------- /ecofreq/ecoctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from datetime import datetime, timedelta 5 | 6 | from ecofreq.config import JOULES_IN_KWH, TS_FORMAT 7 | from ecofreq.ipc import EcoClient 8 | from ecofreq.ecofreq import EcoFreq 9 | 10 | def safe_round(val, digits=0): 11 | return round(val, digits) if (isinstance(val, float)) else val 12 | 13 | def parse_args(): 14 | parser = argparse.ArgumentParser() 15 | cmd_list = ["info", "policy", "provider"] 16 | parser.add_argument("command", choices=cmd_list, default="info", help="Command", nargs="?") 17 | parser.add_argument("cmd_args", nargs="*") 18 | args = parser.parse_args() 19 | return args 20 | 21 | def cmd_info(ec, args): 22 | info = ec.info() 23 | print("EcoFreq is RUNNING") 24 | print("") 25 | print("= CONFIGURATION =") 26 | EcoFreq.print_info(info) 27 | print("") 28 | print("= STATUS =") 29 | print("State: ", info["idle_state"]) 30 | if info["idle_state"] == "IDLE": 31 | print("Idle duration: ", timedelta(seconds = int(info["idle_duration"]))) 32 | print("Load: ", info.get("idle_load", "NA")) 33 | print("Power [W]: ", round(info["avg_power"])) 34 | print("Frequency [MHz]: ", round(info["avg_freq"])) 35 | print("CO2 intensity [g/kWh]: ", info["last_co2kwh"]) 36 | print("Energy price [ct/kWh]: ", safe_round(info["last_price"], 3)) 37 | print("") 38 | print("= STATISTICS =") 39 | ts_start = datetime.strptime(info["start_date"], TS_FORMAT) 40 | uptime = str(datetime.now().replace(microsecond=0) - ts_start) 41 | print("Running since: ", info["start_date"], "(up " + uptime + ")") 42 | print("Energy consumed [kWh]: ", round(float(info["total_energy_j"]) / JOULES_IN_KWH, 3)) 43 | print("CO2 total [kg]: ", round(float(info["total_co2"]) / 1000., 6)) 44 | print("Cost total [EUR]: ", round(float(info["total_cost"]) / 100., 6)) 45 | 46 | def policy_is_enabled(pol, domain="cpu"): 47 | if not domain in pol["co2policy"]: 48 | return False 49 | elif pol["co2policy"][domain].get("governor", "none") == "none": 50 | return False 51 | else: 52 | return True 53 | 54 | def policy_str(pol, domain="cpu"): 55 | d = pol["co2policy"][domain] 56 | return "{0}(governor = {1}, metric = {2})".format(d["control"], d["governor"], d["metric"]) 57 | 58 | def provider_str(prov): 59 | d = prov["co2provider"] 60 | interval = d["provider"]["interval"] 61 | provlist = [] 62 | for m in d["provider"].keys(): 63 | if m not in ["all", "co2", "price"]: 64 | continue 65 | prov_type = d["provider"][m] 66 | if prov_type.startswith("const"): 67 | provstr = prov_type 68 | elif prov_type == "co2signal": 69 | param1 = "Country = " + str(d["co2signal"]["country"]) 70 | # param2 = "Token = " + str(d["co2signal"]["token"]) 71 | provstr = "{0} (interval = {1} s, {2})".format(prov_type, interval, param1) 72 | elif prov_type == "mock": 73 | param1 = "CO2Range = " + str(d["mock"]["co2range"]) 74 | param2 = "CO2File = " + str(d["mock"]["co2file"]) 75 | provstr = "{0} (interval = {1} s, {2}, {3})".format(prov_type, interval, param1, param2) 76 | else: 77 | provstr = "{0} (interval = {1} s)".format(prov_type, interval) 78 | provlist.append(m + " = " + provstr) 79 | return ", ".join(provlist) 80 | 81 | def cmd_policy(ec, args): 82 | policy = ec.get_policy() 83 | 84 | if len(args.cmd_args) > 0: 85 | # set policy 86 | print("Old policy:", policy_str(policy)) 87 | gov = args.cmd_args[0] 88 | domain = "cpu" 89 | if gov.startswith("cpu:") or gov.startswith("gpu:"): 90 | domain, gov = gov.split(":", 1) 91 | if not domain in policy["co2policy"]: 92 | policy["co2policy"][domain] = {} 93 | if gov.startswith("frequency:") or gov.startswith("power:") or gov.startswith("cgroup:"): 94 | control, gov = gov.split(":", 1) 95 | policy["co2policy"][domain]["control"] = control 96 | if gov.startswith("co2:") or gov.startswith("price:") or gov.startswith("fossil_pct:") or gov.startswith("ren_pct:") or gov.startswith("index:"): 97 | metric, gov = gov.split(":", 1) 98 | policy["co2policy"][domain]["metric"] = metric 99 | if gov in ["off", "disabled"]: 100 | gov = "none" 101 | elif gov in ["on", "enabled", "default", "eco"]: 102 | gov = "default" 103 | policy["co2policy"][domain]["governor"] = gov 104 | ret = ec.set_policy(policy) 105 | 106 | policy = ec.get_policy() 107 | print("New policy:", policy_str(policy)) 108 | else: 109 | # get policy 110 | print("CO2 policy:", policy_str(policy)) 111 | 112 | print() 113 | pol_state = "ENABLED" if policy_is_enabled(policy) else "DISABLED" 114 | print("CO2-aware power scaling is now", pol_state) 115 | 116 | def wildcard_set(d, attr, params, idx): 117 | if len(params) > idx and params[idx] != "*": 118 | d[attr] = params[idx] 119 | 120 | def cmd_provider(ec, args): 121 | prov = ec.get_provider() 122 | 123 | # print(prov) 124 | if len(args.cmd_args) > 0: 125 | # set provider 126 | print("Old provider:", provider_str(prov)) 127 | 128 | pstr = args.cmd_args[0] 129 | if pstr.startswith("co2:") or pstr.startswith("price:") or pstr.startswith("fossil_pct:") or pstr.startswith("index:"): 130 | metric, pstr = pstr.split(":", 1) 131 | prov["co2provider"]["provider"]['all'] = None 132 | else: 133 | metric = "all" 134 | 135 | prov_params = pstr.split(":") 136 | p = prov["co2provider"] 137 | wildcard_set(p["provider"], metric, prov_params, 0) 138 | wildcard_set(p["provider"], "interval", prov_params, 1) 139 | prov_type = p["provider"][metric] 140 | if prov_type not in p: 141 | p[prov_type] = {} 142 | if prov_type == "co2signal": 143 | wildcard_set(p["co2signal"], "token", prov_params, 2) 144 | wildcard_set(p["co2signal"], "country", prov_params, 3) 145 | elif prov_type == "mock": 146 | wildcard_set(p["mock"], "co2range", prov_params, 2) 147 | wildcard_set(p["mock"], "co2file", prov_params, 3) 148 | elif prov_type == "const": 149 | wildcard_set(p["const"], metric, prov_params, 2) 150 | # print(prov) 151 | ret = ec.set_provider(prov) 152 | 153 | prov = ec.get_provider() 154 | print("New provider:", provider_str(prov)) 155 | else: 156 | # get policy 157 | print("CO2 provider:", provider_str(prov)) 158 | 159 | def run_command(ec, args): 160 | if args.command == "info": 161 | cmd_info(ec, args) 162 | elif args.command == "policy": 163 | cmd_policy(ec, args) 164 | elif args.command == "provider": 165 | cmd_provider(ec, args) 166 | else: 167 | print("Unknown command:", args.command) 168 | 169 | def main(): 170 | ec = EcoClient() 171 | # print(ec.info()) 172 | 173 | args = parse_args() 174 | # print(args) 175 | 176 | try: 177 | run_command(ec, args) 178 | except ConnectionRefusedError: 179 | print("ERROR: Connection refused! Please check that EcoFreq daemon is running.") 180 | 181 | if __name__ == '__main__': 182 | main() 183 | -------------------------------------------------------------------------------- /ecofreq/policy/cpu.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from inspect import isclass 3 | 4 | from ecofreq.helpers.cpu import CpuInfoHelper, CpuFreqHelper, LinuxPowercapHelper 5 | from ecofreq.helpers.amd import AMDEsmiHelper 6 | from ecofreq.helpers.cgroup import LinuxCgroupHelper, LinuxCgroupV1Helper, LinuxCgroupV2Helper 7 | from ecofreq.helpers.docker import DockerHelper 8 | from ecofreq.policy.common import EcoPolicy 9 | from ecofreq.config import OPTION_DISABLED 10 | 11 | 12 | class CPUEcoPolicy(EcoPolicy): 13 | def __init__(self, config): 14 | EcoPolicy.__init__(self, config) 15 | 16 | @classmethod 17 | def from_config(cls, config): 18 | if not config: 19 | return None 20 | 21 | # first, check if we have a specific EcoPolicy class 22 | c = config["control"] 23 | if c in globals(): 24 | cls = globals()[c] 25 | if isclass(cls) and issubclass(cls, CPUEcoPolicy): 26 | return cls(config) 27 | 28 | # otherwise, look for a generic policy type 29 | c = c.lower() 30 | if c == "auto": 31 | if LinuxPowercapHelper.available() and LinuxPowercapHelper.enabled(): 32 | c = "power" 33 | elif AMDEsmiHelper.available() and AMDEsmiHelper.enabled(): 34 | c = "power" 35 | elif CpuFreqHelper.available(): 36 | c = "frequency" 37 | else: 38 | print ("ERROR: Power management interface not found!") 39 | sys.exit(-1) 40 | 41 | if c == "power": 42 | return CPUPowerEcoPolicy(config) 43 | elif c == "frequency": 44 | return CPUFreqEcoPolicy(config) 45 | elif c == "cgroup": 46 | return CPUCgroupEcoPolicy(config) 47 | elif c == "docker": 48 | return CPUDockerEcoPolicy(config) 49 | elif c in OPTION_DISABLED: 50 | return None 51 | else: 52 | raise ValueError("Unknown policy: " + c) 53 | 54 | class CPUFreqEcoPolicy(CPUEcoPolicy): 55 | UNIT={"MHz": CpuFreqHelper.MHZ} 56 | 57 | def __init__(self, config): 58 | CPUEcoPolicy.__init__(self, config) 59 | self.driver = CpuFreqHelper.get_driver() 60 | 61 | if not self.driver: 62 | print ("ERROR: CPU frequency scaling driver not found!") 63 | sys.exit(-1) 64 | 65 | self.fmin = CpuFreqHelper.get_hw_min_freq() 66 | self.fmax = CpuFreqHelper.get_hw_max_freq() 67 | self.fstart = CpuFreqHelper.get_gov_max_freq() 68 | self.init_governor(config, self.fmin, self.fmax) 69 | 70 | def set_freq(self, freq): 71 | if freq and not self.debug: 72 | #CpuPowerHelper.set_max_freq(freq) 73 | CpuFreqHelper.set_gov_max_freq(freq) 74 | 75 | def set_co2(self, co2): 76 | self.freq = self.co2val(co2) 77 | self.set_freq(self.freq) 78 | 79 | def reset(self): 80 | self.set_freq(self.fmax) 81 | 82 | class CPUPowerEcoPolicy(CPUEcoPolicy): 83 | UNIT={"W": 1} 84 | 85 | def __init__(self, config): 86 | EcoPolicy.__init__(self, config) 87 | 88 | if AMDEsmiHelper.available(): 89 | self.helper = AMDEsmiHelper 90 | else: 91 | self.helper = LinuxPowercapHelper 92 | 93 | if not LinuxPowercapHelper.available(): 94 | print ("ERROR: RAPL powercap driver not found!") 95 | sys.exit(-1) 96 | 97 | if not LinuxPowercapHelper.enabled(): 98 | print ("ERROR: RAPL driver found, but powercap is disabled!") 99 | print ("Please try to enable it as described here: https://askubuntu.com/a/1231490") 100 | print ("If it does not work, switch to frequency control policy.") 101 | sys.exit(-1) 102 | 103 | self.pmax = self.helper.get_package_hw_max_power(0, self.helper.WATT) 104 | self.pmin = 0.1 * self.pmax 105 | self.pstart = self.helper.get_package_power_limit(0, self.helper.WATT) 106 | self.init_governor(config, self.pmin, self.pmax) 107 | 108 | def set_power(self, power_w): 109 | if power_w and not self.debug: 110 | self.helper.set_power_limit(power_w, self.helper.WATT) 111 | 112 | def set_co2(self, co2): 113 | self.power = self.co2val(co2) 114 | # print("Update policy co2 -> power:", co2, "->", self.power) 115 | self.set_power(self.power) 116 | 117 | def reset(self): 118 | self.set_power(self.pmax) 119 | 120 | class CPUCgroupEcoPolicy(CPUEcoPolicy): 121 | UNIT={"c": 1} 122 | 123 | def __init__(self, config): 124 | EcoPolicy.__init__(self, config) 125 | 126 | if not LinuxCgroupV1Helper.available(): 127 | print ("ERROR: Linux cgroup filesystem not mounted!") 128 | sys.exit(-1) 129 | 130 | if LinuxCgroupV1Helper.enabled(): 131 | self.helper = LinuxCgroupV1Helper 132 | elif LinuxCgroupV2Helper.enabled(): 133 | self.helper = LinuxCgroupV2Helper 134 | else: 135 | print ("ERROR: Linux cgroup subsystem is not properly configured!") 136 | sys.exit(-1) 137 | 138 | self.grp = "user.slice" if "cgroup" not in config else config["cgroup"] 139 | self.use_freeze = True if "cgroupfreeze" not in config else config["cgroupfreeze"] 140 | self.use_freeze = self.use_freeze and self.helper.enabled("freezer", self.grp) 141 | 142 | if not self.helper.enabled("cpu", self.grp): 143 | print ("ERROR: Linux cgroup not found or cpu controller is disabled:", self.grp) 144 | sys.exit(-1) 145 | 146 | num_cores = CpuInfoHelper.get_cores() 147 | self.qmax = num_cores 148 | self.qmin = 0 149 | self.qstart = self.helper.get_cpu_quota(self.grp, num_cores) 150 | self.init_governor(config, self.qmin, self.qmax) 151 | 152 | def set_quota(self, quota): 153 | if self.use_freeze: 154 | if quota == self.qmin: 155 | self.helper.freeze(self.grp) 156 | return 157 | else: 158 | self.helper.unfreeze(self.grp) 159 | if quota and not self.debug: 160 | self.helper.set_cpu_quota(self.grp, quota) 161 | 162 | def set_co2(self, co2): 163 | self.quota = self.co2val(co2) 164 | # print("Update policy co2 -> power: ", co2, "->", self.power) 165 | self.set_quota(self.quota) 166 | 167 | def reset(self): 168 | self.set_quota(self.qmax) 169 | 170 | class CPUDockerEcoPolicy(CPUEcoPolicy): 171 | UNIT={"c": 1} 172 | UNLIMITED=0.0 173 | 174 | def __init__(self, config): 175 | EcoPolicy.__init__(self, config) 176 | 177 | if not DockerHelper.available(): 178 | print ("ERROR: Docker not found!") 179 | sys.exit(-1) 180 | 181 | self.ctrs = [] if "containers" not in config else config["containers"].split(",") 182 | self.use_freeze = False if "cgroupfreeze" not in config else config["cgroupfreeze"] 183 | num_cores = CpuInfoHelper.get_cores() if "maxcpus" not in config else float(config["maxcpus"]) 184 | self.qmax = num_cores 185 | self.qmin = 0 186 | self.qstart = self.UNLIMITED 187 | self.init_governor(config, self.qmin, self.qmax) 188 | 189 | def set_quota(self, cpu_quota): 190 | if self.debug: 191 | return 192 | if self.use_freeze: 193 | if cpu_quota == self.qmin: 194 | DockerHelper.set_pause(self.ctrs, True) 195 | return 196 | else: 197 | DockerHelper.set_pause(self.ctrs, False) 198 | if cpu_quota: 199 | DockerHelper.set_container_cpus(self.ctrs, cpu_quota) 200 | 201 | def set_co2(self, co2): 202 | self.quota = self.co2val(co2) 203 | # print("Update policy co2 -> power: ", co2, "->", self.power) 204 | self.set_quota(self.quota) 205 | 206 | def reset(self): 207 | self.set_quota(self.qstart) 208 | -------------------------------------------------------------------------------- /ecofreq/ecostat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from datetime import datetime, timedelta 5 | import os 6 | import argparse 7 | 8 | from ecofreq import __version__ 9 | from ecofreq.config import TS_FORMAT, JOULES_IN_KWH 10 | 11 | LOG_FILE = "/var/log/ecofreq.log" 12 | DATE_FORMAT = "%Y-%m-%d" 13 | 14 | FIELD_TS = "Timestamp" 15 | FIELD_CO2KWH = "gCO2/kWh" 16 | FIELD_FMAX = "Fmax [Mhz]" 17 | FIELD_FAVG = "Favg [Mhz]" 18 | FIELD_PMAX = "CPU_Pmax [W]" 19 | FIELD_PAVG = "SYS_Pavg [W]" 20 | FIELD_ENERGY = "Energy [J]" 21 | FIELD_CO2 = "CO2 [g]" 22 | FIELD_IDLE = "State" 23 | LOG_FIELDS = [FIELD_TS, FIELD_CO2KWH, FIELD_FMAX, FIELD_FAVG, FIELD_PMAX, FIELD_PAVG, FIELD_ENERGY, FIELD_CO2] 24 | 25 | def parse_timestamp(ts_str, exit_on_error=False): 26 | ts = None 27 | for fmt in TS_FORMAT, DATE_FORMAT: 28 | try: 29 | ts = datetime.strptime(ts_str.strip(), fmt) 30 | except ValueError: 31 | pass 32 | 33 | if not ts and exit_on_error: 34 | print("ERROR: Invalid date/time: ", ts_str) 35 | sys.exit(-1) 36 | else: 37 | return ts 38 | 39 | class EcoStat(object): 40 | def __init__(self, args): 41 | self.log_fname = args.log_fname 42 | self.samples = 0 43 | self.energy = 0 44 | self.co2 = 0 45 | self.co2kwh_min = 1e6 46 | self.co2kwh_max = 0 47 | self.co2kwh_avg = 0 48 | self.timestamp_min = datetime.max 49 | self.timestamp_max = datetime.min 50 | self.duration = timedelta(seconds = 0) 51 | self.gap_duration = timedelta(seconds = 0) 52 | self.idle_duration = timedelta(seconds = 0) 53 | self.idle_energy = 0 54 | self.idle_co2 = 0 55 | 56 | if args.ts_start: 57 | self.ts_start = parse_timestamp(args.ts_start, True) 58 | else: 59 | self.ts_start = datetime.min 60 | 61 | if args.ts_end: 62 | self.ts_end = parse_timestamp(args.ts_end, True) 63 | else: 64 | self.ts_end = datetime.max 65 | 66 | if self.ts_end == self.ts_start: 67 | self.ts_end += timedelta(days=1) 68 | 69 | if self.ts_end < self.ts_start: 70 | print("ERROR: End date is earlier than start date! start =", self.ts_start, ", end=", self.ts_end) 71 | sys.exit(-1) 72 | 73 | if not os.path.isfile(self.log_fname): 74 | print("ERROR: Log file not found: ", self.log_fname) 75 | sys.exit(-1) 76 | 77 | self.fields = LOG_FIELDS 78 | self.update_field_idx() 79 | 80 | def field_idx(self, field_name): 81 | try: 82 | return self.fields.index(field_name) 83 | except ValueError: 84 | return None 85 | 86 | def update_field_idx(self): 87 | self.time_idx = self.field_idx(FIELD_TS) 88 | self.co2kwh_idx = self.field_idx(FIELD_CO2KWH) 89 | self.energy_idx = self.field_idx(FIELD_ENERGY) 90 | self.co2_idx = self.field_idx(FIELD_CO2) 91 | self.idle_idx = self.field_idx(FIELD_IDLE) 92 | 93 | def parse_header(self, line): 94 | self.fields = [x.strip() for x in line.replace("#", "", 1).split("\t")] 95 | self.update_field_idx() 96 | 97 | def parse_command(self, line): 98 | line = line.replace(" ", "\t") 99 | toks = [x.strip() for x in line.replace("##", "", 1).split("\t")] 100 | ts = datetime.strptime(toks[0].strip(), TS_FORMAT) 101 | cmd = toks[1].lower() 102 | return ts, cmd, toks[2:] 103 | 104 | def compute_stats(self): 105 | print("Loading data from log file:", self.log_fname, "\n") 106 | last_ts = None 107 | gap_start_ts = None 108 | co2kwh_sum = 0 109 | co2_samples = 0 110 | co2_na_energy = 0 111 | idle_na_energy = 0 112 | duration_samples = 0 113 | state_samples = 0 114 | with open(self.log_fname) as f: 115 | for line in f: 116 | if line.startswith("##"): 117 | ts, cmd, args = self.parse_command(line) 118 | # print(ts, cmd, args) 119 | last_ts = ts if cmd in ["start"] else None 120 | continue 121 | elif line.startswith("#"): 122 | self.parse_header(line) 123 | gap_start_ts = last_ts 124 | last_ts = None 125 | continue 126 | toks = line.split("\t") 127 | 128 | sample_idle = False 129 | 130 | ts = datetime.strptime(toks[self.time_idx].strip(), TS_FORMAT) 131 | if ts < self.ts_start or ts > self.ts_end: 132 | continue 133 | self.timestamp_min = min(self.timestamp_min, ts) 134 | self.timestamp_max = max(self.timestamp_max, ts) 135 | if last_ts: 136 | self.duration += (ts - last_ts) 137 | duration_samples += 1 138 | if self.idle_idx: 139 | if toks[self.idle_idx].strip() == "IDLE": 140 | sample_idle = True 141 | self.idle_duration += (ts - last_ts) 142 | state_samples += 1 143 | elif gap_start_ts: 144 | self.gap_duration += (ts - gap_start_ts) 145 | gap_start_ts = None 146 | last_ts = ts 147 | 148 | energy = float(toks[self.energy_idx]) 149 | self.energy += energy 150 | if sample_idle: 151 | self.idle_energy += energy 152 | 153 | co2kwh = toks[self.co2kwh_idx].strip() 154 | if co2kwh != "NA": 155 | co2kwh = float(co2kwh) 156 | co2kwh_sum += co2kwh 157 | co2_samples += 1 158 | self.co2kwh_min = min(self.co2kwh_min, co2kwh) 159 | self.co2kwh_max = max(self.co2kwh_max, co2kwh) 160 | sample_co2 = float(toks[self.co2_idx]) 161 | else: 162 | sample_co2 = None 163 | co2_na_energy += energy 164 | if sample_idle: 165 | idle_na_energy += energy 166 | 167 | if sample_co2: 168 | self.co2 += sample_co2 169 | if sample_idle: 170 | self.idle_co2 += sample_co2 171 | 172 | self.samples += 1 173 | 174 | if co2_samples > 0: 175 | self.co2kwh_avg = co2kwh_sum / co2_samples 176 | self.co2 += self.co2kwh_avg * (co2_na_energy / JOULES_IN_KWH) 177 | if idle_na_energy: 178 | self.idle_co2 += self.co2kwh_avg * (idle_na_energy / JOULES_IN_KWH) 179 | 180 | if state_samples == duration_samples: 181 | self.idle_prop = self.idle_duration / self.duration 182 | else: 183 | self.idle_prop = None 184 | 185 | def print_stats(self): 186 | if self.samples> 0: 187 | print ("Time interval: ", self.timestamp_min, "-", self.timestamp_max) 188 | print ("Monitoring active: ", self.duration) 189 | print ("Monitoring inactive: ", self.gap_duration) 190 | print ("CO2 intensity range [g/kWh]:", round(self.co2kwh_min), "-", round(self.co2kwh_max)) 191 | print ("CO2 intensity mean [g/kWh]: ", round(self.co2kwh_avg)) 192 | print ("Energy consumed [J]: ", round(self.energy, 3)) 193 | print ("Energy consumed [kWh]: ", round(self.energy / JOULES_IN_KWH, 3)) 194 | print ("= electric car travel [km]: ", round(self.energy / JOULES_IN_KWH / 0.2)) 195 | print ("Total CO2 emitted [kg]: ", round(self.co2 / 1000., 6)) 196 | 197 | if self.idle_duration: 198 | print("") 199 | print ("Idle time: ", self.idle_duration) 200 | if self.idle_prop: 201 | print ("Idle time proportion: ", round(self.idle_prop, 2)) 202 | print ("Idle energy [kWh]: ", round(self.idle_energy / JOULES_IN_KWH, 3)) 203 | print ("Idle = e-car travel [km]: ", round(self.idle_energy / JOULES_IN_KWH / 0.2)) 204 | print ("Idle CO2 [kg]: ", round(self.idle_co2 / 1000., 6)) 205 | 206 | else: 207 | print ("No samples found in the given time interval!") 208 | print("") 209 | 210 | 211 | def parse_args(): 212 | parser = argparse.ArgumentParser() 213 | parser.add_argument("-c", dest="cfg_file", default=None, help="Config file name.") 214 | parser.add_argument("-l", dest="log_fname", default=LOG_FILE, help="Log file name.") 215 | parser.add_argument("--start", dest="ts_start", default=None, help="Start date/time (format: yy-mm-ddTHH:MM:SS).") 216 | parser.add_argument("--end", dest="ts_end", default=None, help="End date/time (format: yy-mm-ddTHH:MM:SS).") 217 | args = parser.parse_args() 218 | return args 219 | 220 | def main(): 221 | args = parse_args() 222 | 223 | print(f"EcoStat v{__version__}\n") 224 | 225 | es = EcoStat(args) 226 | es.compute_stats() 227 | es.print_stats() 228 | 229 | if __name__ == '__main__': 230 | main() -------------------------------------------------------------------------------- /ecofreq/monitors/energy.py: -------------------------------------------------------------------------------- 1 | from ecofreq.config import OPTION_DISABLED 2 | from ecofreq.helpers.cpu import CpuInfoHelper, CpuFreqHelper, LinuxPowercapHelper 3 | from ecofreq.helpers.amd import AMDRaplMsrHelper 4 | from ecofreq.helpers.nvidia import NvidiaGPUHelper 5 | from ecofreq.helpers.ipmi import IPMIHelper 6 | from ecofreq.monitors.common import Monitor 7 | from ecofreq.mqtt import MQTTManager 8 | 9 | class EnergyMonitor(Monitor): 10 | def __init__(self, config): 11 | Monitor.__init__(self, config) 12 | self.total_energy = 0 13 | self.period_energy = 0 14 | self.last_avg_power = 0 15 | self.monitor_freq = CpuFreqHelper.available() 16 | 17 | @classmethod 18 | def from_config(cls, config): 19 | sens_dict = {"rapl" : PowercapEnergyMonitor, "amd_msr" : AMDMsrEnergyMonitor, "ipmi" : IPMIEnergyMonitor, "gpu" : GPUEnergyMonitor } 20 | p = config["monitor"].get("PowerSensor", "auto").lower() 21 | monitors = [] 22 | if p in OPTION_DISABLED: 23 | pass 24 | elif p == "auto": 25 | if IPMIEnergyMonitor.available(): 26 | monitors.append(IPMIEnergyMonitor(config)) 27 | else: 28 | if PowercapEnergyMonitor.available(): 29 | monitors.append(PowercapEnergyMonitor(config)) 30 | elif AMDMsrEnergyMonitor.available(): 31 | monitors.append(AMDMsrEnergyMonitor(config)) 32 | if NvidiaGPUHelper.available(): 33 | monitors.append(GPUEnergyMonitor(config)) 34 | elif p == "mqtt": 35 | monitors.append(MQTTEnergyMonitor(config)) 36 | else: 37 | for s in p.split(","): 38 | if s in sens_dict: 39 | monitors.append(sens_dict[s](config)) 40 | else: 41 | raise ValueError("Unknown power sensor: " + p) 42 | return monitors 43 | 44 | def update_energy(self): 45 | energy = self.sample_energy() 46 | # print("energy diff:", energy) 47 | self.last_avg_power = energy / self.interval 48 | self.total_energy += energy 49 | self.period_energy += energy 50 | 51 | def update_impl(self): 52 | self.update_energy() 53 | 54 | def get_period_energy(self): 55 | return self.period_energy 56 | 57 | def get_total_energy(self): 58 | return self.total_energy 59 | 60 | def get_last_avg_power(self): 61 | return self.last_avg_power 62 | 63 | def get_period_avg_power(self): 64 | if self.period_samples: 65 | return self.period_energy / (self.period_samples * self.interval) 66 | else: 67 | return 0 68 | 69 | def get_total_avg_power(self): 70 | if self.total_samples: 71 | return self.total_energy / (self.total_samples * self.interval) 72 | else: 73 | return 0 74 | 75 | def reset_period(self): 76 | Monitor.reset_period(self) 77 | self.period_energy = 0 78 | 79 | class NoEnergyMonitor(EnergyMonitor): 80 | def update_energy(self): 81 | pass 82 | 83 | class RAPLEnergyMonitor(EnergyMonitor): 84 | UJOULE, JOULE, WH = 1, 1e6, 3600*1e6 85 | 86 | def __init__(self, config): 87 | EnergyMonitor.__init__(self, config) 88 | c = config['powercap'] if 'powercap' in config else {} 89 | self.estimate_full_power = c.get('EstimateFullPower', True) 90 | self.syspower_coeff_const = c.get('FullPowerConstCoeff', 0.3) 91 | self.syspower_coeff_var = c.get('FullPowerVarCoeff', 0.25) 92 | self.psys_domain = False 93 | self.init_pkg_list() 94 | self.init_energy() 95 | 96 | def init_energy(self): 97 | self.last_energy = {} 98 | self.energy_range = {} 99 | for p in self.pkg_list: 100 | self.last_energy[p] = 0 101 | self.energy_range[p] = self.get_package_energy_range(p) 102 | self.sample_energy() 103 | 104 | def full_system_energy(self, energy_diff): 105 | if self.psys_domain or not self.estimate_full_power: 106 | return energy_diff 107 | else: 108 | sysenergy_const = self.cpu_max_power_uw * self.syspower_coeff_const * self.interval 109 | sysenegy_var = (1. + self.syspower_coeff_var) * energy_diff 110 | return sysenergy_const + sysenegy_var 111 | 112 | def sample_energy(self): 113 | energy_diff = 0 114 | for p in self.pkg_list: 115 | new_energy = self.get_package_energy(p) 116 | if new_energy >= self.last_energy[p]: 117 | diff_uj = new_energy - self.last_energy[p] 118 | else: 119 | diff_uj = new_energy + (self.energy_range[p] - self.last_energy[p]); 120 | self.last_energy[p] = new_energy 121 | energy_diff += diff_uj 122 | energy_diff = self.full_system_energy(energy_diff) 123 | energy_diff_j = energy_diff / self.JOULE 124 | return energy_diff_j 125 | 126 | class PowercapEnergyMonitor(RAPLEnergyMonitor): 127 | 128 | @classmethod 129 | def available(cls): 130 | try: 131 | energy = LinuxPowercapHelper.get_package_energy(0) 132 | return energy > 0 133 | except OSError: 134 | return False 135 | 136 | def __init__(self, config): 137 | RAPLEnergyMonitor.__init__(self, config) 138 | 139 | def init_pkg_list(self): 140 | self.pkg_list = LinuxPowercapHelper.package_list("psys") 141 | if self.pkg_list: 142 | self.psys_domain = True 143 | else: 144 | self.psys_domain = False 145 | self.pkg_list = LinuxPowercapHelper.package_list("package-") 146 | if self.pkg_list: 147 | if LinuxPowercapHelper.available(): 148 | self.cpu_max_power_uw = LinuxPowercapHelper.get_package_hw_max_power(self.pkg_list[0]) 149 | else: 150 | self.cpu_max_power_uw = CpuInfoHelper.get_tdp_uw() 151 | self.pkg_list += LinuxPowercapHelper.package_list("dram") 152 | 153 | def get_package_energy(self, pkg): 154 | return LinuxPowercapHelper.get_package_energy(pkg) 155 | 156 | def get_package_energy_range(self, pkg): 157 | return LinuxPowercapHelper.get_package_energy_range(pkg) 158 | 159 | class AMDMsrEnergyMonitor(RAPLEnergyMonitor): 160 | 161 | @classmethod 162 | def available(cls): 163 | try: 164 | energy = AMDRaplMsrHelper.get_package_energy(0) 165 | return energy > 0 166 | except OSError: 167 | return False 168 | 169 | def __init__(self, config): 170 | RAPLEnergyMonitor.__init__(self, config) 171 | 172 | def init_pkg_list(self): 173 | self.pkg_list = AMDRaplMsrHelper.package_list() 174 | self.cpu_max_power_uw = CpuInfoHelper.get_tdp_uw() 175 | 176 | def get_package_energy(self, pkg): 177 | return AMDRaplMsrHelper.get_package_energy(pkg) 178 | 179 | def get_package_energy_range(self, pkg): 180 | return AMDRaplMsrHelper.get_package_energy_range(pkg) 181 | 182 | 183 | class GPUEnergyMonitor(EnergyMonitor): 184 | def __init__(self, config): 185 | EnergyMonitor.__init__(self, config) 186 | self.last_pwr = 0 187 | 188 | @classmethod 189 | def available(cls): 190 | return NvidiaGPUHelper.available() 191 | 192 | def sample_energy(self): 193 | cur_pwr = NvidiaGPUHelper.get_power() 194 | if not cur_pwr: 195 | print ("WARNING: GPU power reading failed!") 196 | cur_pwr = self.last_pwr 197 | avg_pwr = 0.5 * (self.last_pwr + cur_pwr) 198 | energy_diff = avg_pwr * self.interval 199 | self.last_pwr = cur_pwr 200 | return energy_diff 201 | 202 | class IPMIEnergyMonitor(EnergyMonitor): 203 | def __init__(self, config): 204 | EnergyMonitor.__init__(self, config) 205 | self.last_pwr = 0 206 | 207 | @classmethod 208 | def available(cls): 209 | return IPMIHelper.available() 210 | 211 | def sample_energy(self): 212 | cur_pwr = IPMIHelper.get_power() 213 | if not cur_pwr: 214 | print ("WARNING: IPMI power reading failed!") 215 | cur_pwr = self.last_pwr 216 | avg_pwr = 0.5 * (self.last_pwr + cur_pwr) 217 | energy_diff = avg_pwr * self.interval 218 | self.last_pwr = cur_pwr 219 | return energy_diff 220 | 221 | class MQTTEnergyMonitor(EnergyMonitor): 222 | def __init__(self, config): 223 | EnergyMonitor.__init__(self, config) 224 | self.last_pwr = 0 225 | self.label = "mqtt_power" 226 | mqtt_cfg = config[self.label] 227 | self.interval = int(mqtt_cfg.get("interval", self.interval)) 228 | self.mqtt_client = MQTTManager.add_client(self.label, mqtt_cfg) 229 | 230 | @classmethod 231 | def available(cls): 232 | return True 233 | 234 | def sample_energy(self): 235 | cur_pwr = self.mqtt_client.get_msg() 236 | if not cur_pwr: 237 | print ("WARNING: MQTT power reading failed!") 238 | cur_pwr = self.last_pwr 239 | else: 240 | cur_pwr = float(cur_pwr) 241 | avg_pwr = 0.5 * (self.last_pwr + cur_pwr) 242 | energy_diff = avg_pwr * self.interval 243 | self.last_pwr = cur_pwr 244 | return energy_diff 245 | 246 | -------------------------------------------------------------------------------- /ecofreq/helpers/cpu.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from subprocess import call,check_output,DEVNULL,CalledProcessError 3 | 4 | from ecofreq.utils import * 5 | from ecofreq.config import DATADIR 6 | 7 | class CpuInfoHelper(object): 8 | CMD_LSCPU = "lscpu" 9 | CPU_TDP_FILE = DATADIR / "cpu_tdp.csv" 10 | 11 | @classmethod 12 | def available(cls): 13 | return call(cls.CMD_LSCPU, shell=True, stderr=DEVNULL) == 0 14 | 15 | @classmethod 16 | def parse_lscpu(cls): 17 | try: 18 | out = check_output(cls.CMD_LSCPU, shell=True, stderr=DEVNULL, universal_newlines=True) 19 | cpuinfo = {} 20 | for line in out.split("\n"): 21 | tok = line.split(":") 22 | if len(tok) > 1: 23 | cpuinfo[tok[0]] = tok[1].strip() 24 | return cpuinfo 25 | except CalledProcessError: 26 | return None 27 | 28 | @classmethod 29 | def get_cores(cls): 30 | cpuinfo = cls.parse_lscpu() 31 | threads = int(cpuinfo["CPU(s)"]) 32 | cores = int(threads / int(cpuinfo["Thread(s) per core"])) 33 | return cores 34 | 35 | @classmethod 36 | def get_sockets(cls): 37 | cpuinfo = cls.parse_lscpu() 38 | return int(cpuinfo["Socket(s)"]) 39 | 40 | @classmethod 41 | def get_tdp_uw(cls): 42 | cpuinfo = cls.parse_lscpu() 43 | mymodel = cpuinfo["Model name"] 44 | mymodel = mymodel.split(" with ")[0] 45 | mycpu_toks = [] 46 | for w in mymodel.split(" "): 47 | if w.lower().endswith("-core"): 48 | break 49 | if w.lower() in ["processor"]: 50 | continue 51 | mycpu_toks.append(w) 52 | mycpu = " ".join(mycpu_toks) 53 | 54 | with open(cls.CPU_TDP_FILE) as f: 55 | for line in f: 56 | model, tdp = line.rstrip('\n').split(",") 57 | if model == mycpu: 58 | return float(tdp.rstrip("W")) * 1e6 59 | 60 | return None 61 | 62 | @classmethod 63 | def info(cls): 64 | cpuinfo = cls.parse_lscpu() 65 | if cpuinfo: 66 | model = cpuinfo["Model name"] 67 | sockets = int(cpuinfo["Socket(s)"]) 68 | threads = int(cpuinfo["CPU(s)"]) 69 | cores = int(threads / int(cpuinfo["Thread(s) per core"])) 70 | print("CPU model: ", model) 71 | print("CPU sockets/cores/threads:", sockets, "/", cores, "/", threads) 72 | else: 73 | print("CPU info not available") 74 | 75 | class CpuFreqHelper(object): 76 | SYSFS_CPU_PATH = "/sys/devices/system/cpu/cpu{0}/cpufreq/{1}" 77 | KHZ, MHZ, GHZ = 1, 1e3, 1e6 78 | 79 | @classmethod 80 | def cpu_field_fname(cls, cpu, field): 81 | return cls.SYSFS_CPU_PATH.format(cpu, field) 82 | 83 | @classmethod 84 | def available(cls): 85 | return os.path.isfile(cls.cpu_field_fname(0, "scaling_driver")) 86 | 87 | @classmethod 88 | def info(cls): 89 | if cls.available(): 90 | print ("DVFS settings: driver = " + cls.get_driver() + ", governor = " + cls.get_governor()) 91 | hw_fmin = round(cls.get_hw_min_freq(0, cls.MHZ)) 92 | hw_fmax = round(cls.get_hw_max_freq(0, cls.MHZ)) 93 | gov_fmin = round(cls.get_gov_min_freq(0, cls.MHZ)) 94 | gov_fmax = round(cls.get_gov_max_freq(0, cls.MHZ)) 95 | print ("DVFS HW limits: " + str(hw_fmin) + " - " + str(hw_fmax) + " MHz") 96 | print ("DVFS policy: " + str(gov_fmin) + " - " + str(gov_fmax) + " MHz") 97 | else: 98 | print("DVFS driver not found.") 99 | 100 | @classmethod 101 | def get_string(cls, name, cpu=0): 102 | try: 103 | return read_value(cls.cpu_field_fname(cpu, name)) 104 | except: 105 | return None 106 | 107 | @classmethod 108 | def get_int(cls, name, cpu=0): 109 | s = cls.get_string(name, cpu) 110 | return None if s is None else int(s) 111 | 112 | @classmethod 113 | def get_int_scaled(cls, name, cpu=0, unit=KHZ): 114 | s = cls.get_string(name, cpu) 115 | if s: 116 | return int(s) / unit 117 | else: 118 | return None 119 | 120 | @classmethod 121 | def get_driver(cls): 122 | if cls.available(): 123 | return cls.get_string("scaling_driver").strip() 124 | else: 125 | return None 126 | 127 | @classmethod 128 | def get_governor(cls): 129 | return cls.get_string("scaling_governor").strip() 130 | 131 | @classmethod 132 | def get_hw_min_freq(cls, cpu=0, unit=KHZ): 133 | return cls.get_int_scaled("cpuinfo_min_freq", cpu, unit) 134 | 135 | @classmethod 136 | def get_hw_max_freq(cls, cpu=0, unit=KHZ): 137 | return cls.get_int_scaled("cpuinfo_max_freq", cpu, unit) 138 | 139 | @classmethod 140 | def get_hw_cur_freq(cls, cpu=0, unit=KHZ): 141 | return cls.get_int_scaled("cpuinfo_cur_freq", cpu, unit) 142 | 143 | @classmethod 144 | def get_gov_min_freq(cls, cpu=0, unit=KHZ): 145 | return cls.get_int_scaled("scaling_min_freq", cpu, unit) 146 | 147 | @classmethod 148 | def get_gov_max_freq(cls, cpu=0, unit=KHZ): 149 | return cls.get_int_scaled("scaling_max_freq", cpu, unit) 150 | 151 | @classmethod 152 | def get_gov_cur_freq(cls, cpu=0, unit=KHZ): 153 | return cls.get_int_scaled("scaling_cur_freq", cpu, unit) 154 | 155 | @classmethod 156 | def get_avg_gov_cur_freq(cls, unit=KHZ): 157 | cpu = 0 158 | fsum = 0 159 | while True: 160 | fcpu = cls.get_gov_cur_freq(cpu, unit) 161 | if fcpu: 162 | fsum += fcpu 163 | cpu += 1 164 | else: 165 | break 166 | return fsum / cpu 167 | 168 | @classmethod 169 | def set_cpu_field_value(cls, cpu, field, value): 170 | return write_value(cls.cpu_field_fname(cpu, field), value) 171 | 172 | @classmethod 173 | def set_field_value(cls, field, value): 174 | cpu = 0 175 | while cls.set_cpu_field_value(cpu, field, value): 176 | cpu += 1 177 | 178 | @classmethod 179 | def set_gov_max_freq(cls, freq): 180 | cls.set_field_value("scaling_max_freq", freq) 181 | 182 | class CpuPowerHelper(object): 183 | @classmethod 184 | def set_max_freq(cls, freq): 185 | call("cpupower frequency-set -u " + str(freq) + " > /dev/null", shell=True) 186 | 187 | class LinuxPowercapHelper(object): 188 | INTEL_RAPL_PATH="/sys/class/powercap/intel-rapl:" 189 | PKG_MAX=256 190 | UWATT, MWATT, WATT = 1, 1e3, 1e6 191 | 192 | @classmethod 193 | def package_path(cls, pkg): 194 | return cls.INTEL_RAPL_PATH + str(pkg) 195 | 196 | @classmethod 197 | def package_file(cls, pkg, fname): 198 | return os.path.join(cls.package_path(pkg), fname) 199 | 200 | @classmethod 201 | def read_package_int(cls, pkg, fname): 202 | return read_int_value(cls.package_file(pkg, fname)) 203 | 204 | @classmethod 205 | def package_list(cls, domain="package-"): 206 | l = [] 207 | pkg = 0 208 | while pkg < cls.PKG_MAX: 209 | fname = cls.package_file(pkg, "name") 210 | if not os.path.isfile(fname): 211 | break; 212 | pkg_name = read_value(fname) 213 | if pkg_name.startswith(domain): 214 | l += [str(pkg)] 215 | if domain in ["dram", "core", "uncore"]: 216 | subpkg = 0 217 | while subpkg < cls.PKG_MAX: 218 | subpkg_code = str(pkg) + ":" + str(subpkg) 219 | fname = cls.package_file(subpkg_code, "name") 220 | if not os.path.isfile(fname): 221 | break; 222 | pkg_name = read_value(fname) 223 | if pkg_name.startswith(domain): 224 | l += [subpkg_code] 225 | subpkg += 1 226 | pkg += 1 227 | return l 228 | 229 | @classmethod 230 | def available(cls, readonly=False): 231 | if readonly: 232 | return os.path.isfile(cls.package_file(0, "energy_uj")) 233 | else: 234 | return os.path.isfile(cls.package_file(0, "constraint_0_power_limit_uw")) 235 | 236 | @classmethod 237 | def enabled(cls, pkg=0): 238 | return cls.read_package_int(pkg, "enabled") != 0 239 | 240 | @classmethod 241 | def info(cls): 242 | if cls.available(True): 243 | outfmt = "RAPL {0} domains: count = {1}, hw_limit = {2} W, current_limit = {3} W" 244 | cpus = cls.package_list() 245 | if len(cpus): 246 | maxp = cls.get_package_hw_max_power(cpus[0], cls.WATT) 247 | curp = cls.get_package_power_limit(cpus[0], cls.WATT) 248 | print(outfmt.format("CPU ", len(cpus), maxp, curp)) 249 | dram = cls.package_list("dram") 250 | if len(dram): 251 | try: 252 | maxp = cls.get_package_hw_max_power(dram[0], cls.WATT) 253 | except OSError: 254 | maxp = None 255 | curp = cls.get_package_power_limit(dram[0], cls.WATT) 256 | print(outfmt.format("DRAM", len(dram), maxp, curp)) 257 | psys = cls.package_list("psys") 258 | if len(psys): 259 | try: 260 | maxp = cls.get_package_hw_max_power(psys[0], cls.WATT) 261 | except OSError: 262 | maxp = None 263 | curp = cls.get_package_power_limit(psys[0], cls.WATT) 264 | print(outfmt.format("PSYS", len(psys), maxp, curp)) 265 | else: 266 | print("RAPL powercap not found.") 267 | 268 | @classmethod 269 | def get_package_hw_max_power(cls, pkg, unit=UWATT): 270 | if cls.available(): 271 | return cls.read_package_int(pkg, "constraint_0_max_power_uw") / unit 272 | else: 273 | return None 274 | 275 | @classmethod 276 | def get_package_power_limit(cls, pkg, unit=UWATT): 277 | if cls.available(): 278 | return cls.read_package_int(pkg, "constraint_0_power_limit_uw") / unit 279 | else: 280 | return None 281 | 282 | @classmethod 283 | def get_package_energy(cls, pkg): 284 | return cls.read_package_int(pkg, "energy_uj") 285 | 286 | @classmethod 287 | def get_package_energy_range(cls, pkg): 288 | return cls.read_package_int(pkg, "max_energy_range_uj") 289 | 290 | @classmethod 291 | def get_power_limit(cls, unit=UWATT): 292 | power = 0 293 | for pkg in cls.package_list(): 294 | power += cls.get_package_power_limit(pkg, unit) 295 | return power 296 | 297 | @classmethod 298 | def set_package_power_limit(cls, pkg, power, unit=UWATT): 299 | val = round(power * unit) 300 | write_value(cls.package_file(pkg, "constraint_0_power_limit_uw"), val) 301 | 302 | @classmethod 303 | def reset_package_power_limit(cls, pkg): 304 | # write_value(cls.package_file(pkg, "constraint_0_power_limit_uw"), round(cls.get_package_hw_max_power(pkg))) 305 | cls.set_package_power_limit(pkg, cls.get_package_hw_max_power(pkg)) 306 | 307 | @classmethod 308 | def set_power_limit(cls, power, unit=UWATT): 309 | for pkg in cls.package_list(): 310 | cls.set_package_power_limit(pkg, power, unit) 311 | -------------------------------------------------------------------------------- /ecofreq/data/cpu_tdp.csv: -------------------------------------------------------------------------------- 1 | AMD 3015e,6W 2 | AMD 3015Ce,6W 3 | AMD 3020e,6W 4 | 7th Gen A9-9420 APU,15W 5 | 7th Gen A6-9220 APU,15W 6 | 7th Gen A4-9120 APU,15W 7 | 7th Gen A12-9730P APU,35W 8 | 7th Gen A12-9700P APU,15W 9 | 7th Gen A10-9630P APU,35W 10 | 7th Gen A10-9600P APU,15W 11 | 7th Gen A9-9410 APU,25W 12 | 7th Gen A6-9210 APU,15W 13 | 7th Gen A12-9800 APU,65W 14 | 7th Gen A10-9700 APU,65W 15 | 7th Gen A8-9600 APU,65W 16 | 7th Gen A6-9500E APU,35W 17 | 7th Gen A6-9500 APU,65W 18 | 7th Gen A12-9800E APU,35W 19 | 7th Gen A10-9700E APU,35W 20 | 7th Gen A6-9550 APU,65W 21 | 7th Gen A9-9425 APU,15W 22 | 7th Gen A6-9225 APU,15W 23 | 7th Gen A4-9125 APU,15W 24 | A10-8700P,35W 25 | A10-7890K,95W 26 | A10-7870K,95W 27 | A10-7870K,95W 28 | A10-7860K,65W 29 | A10-7860K,65W 30 | A10-7850K,95W 31 | A10-7800,45W 32 | A10-7700K,45W 33 | A10-7400P,35W 34 | A10-7300,20W 35 | A10-6800K,100W 36 | A10-6800B,45W 37 | A10-6790K,100W 38 | A10-6790B,45W 39 | A10-6700T,45W 40 | A10-6700,65W 41 | A10 Micro-6700T,4.5W 42 | A8-8600P,15W 43 | A8-7670K,95W 44 | A8-7650K,95W 45 | A8-7650K,95W 46 | A8-7600,65W 47 | A8-7410,15W 48 | A8-7200P,100W 49 | A8-7100,20W 50 | A8-6600K,65W 51 | A8-6500T,45W 52 | A8-6500B,65W 53 | A8-6500,65W 54 | A8-6410,15W 55 | A8-6410,15W 56 | A6-7310,15W 57 | A6-7310,15W 58 | A6-7310,15W 59 | A6-6310,15W 60 | A6-6310,15W 61 | A6-5200M,25W 62 | A6-5200,25W 63 | A4-7210,65W 64 | A4-7210,65W 65 | A4-6210,15W 66 | A4-5100,15W 67 | A4-5100,15W 68 | A4-5000,15W 69 | A4-5000,15W 70 | A4 Micro-6400T,4.5W 71 | 6th Gen A10-8700P APU,15W 72 | 6th Gen A8-8600P APU,15W 73 | A6-8500P,15W 74 | A6-8500P,15W 75 | A6-7470K,65W 76 | A6-7400K,65W 77 | A6-7000,17W 78 | A6-6420K,65W 79 | A6-6420B,65W 80 | A6-6400K,65W 81 | A6-6400B,65W 82 | A6-5350M,35W 83 | A4-7300,65W 84 | A4-6320,65W 85 | A4-6300,65W 86 | 7th Gen A6-9220C APU,6W 87 | 7th Gen A4-9120C APU,6W 88 | AMD Athlon PRO 3145B,15W 89 | AMD Athlon PRO 3045B,15W 90 | AMD Athlon PRO 300GE,35W 91 | AMD Athlon Gold PRO 3150GE,35W 92 | AMD Athlon Gold PRO 3150G,65W 93 | AMD Athlon Silver PRO 3125GE,35W 94 | AMD Athlon PRO 300U Mobile,15W 95 | AMD Athlon PRO 200U Mobile,15W 96 | AMD Athlon PRO 200GE,35W 97 | AMD Athlon Gold 3150C,15W 98 | AMD Athlon Silver 3050C,15W 99 | 7th Gen AMD Athlon X4 970,65W 100 | 7th Gen AMD Athlon X4 950,45W 101 | 7th Gen AMD Athlon X4 940,45W 102 | AMD Athlon Gold 3150GE ,35W 103 | AMD Athlon Gold 3150G ,65W 104 | AMD Athlon Silver 3050GE ,35W 105 | AMD Athlon 320GE,35W 106 | AMD Athlon 300GE,35W 107 | AMD Athlon Silver 3050e,6W 108 | AMD Athlon Silver 3050U,15W 109 | AMD Athlon Gold 3150U,15W 110 | Athlon 5370 APU,25W 111 | Athlon 5350 APU,25W 112 | Athlon 5150 APU,25W 113 | AMD Athlon 3000G,35W 114 | AMD Athlon 300U,15W 115 | AMD Athlon 240GE,35W 116 | AMD Athlon 220GE,35W 117 | AMD Athlon 200GE,35W 118 | 7th Gen E2-9010 APU,15W 119 | E2-7110,65W 120 | E2-7110,65W 121 | E2-7110,65W 122 | E2-6110,15W 123 | E2-3800,15W 124 | E2-3000,15W 125 | E1-7010,10W 126 | E1-7010,10W 127 | E1-6010,10W 128 | E1-2500,15W 129 | E1-2200,9W 130 | E1-2100,9W 131 | E1 Micro-6200T,3.95W 132 | AMD EPYC 7763,280W 133 | AMD EPYC 7713P,225W 134 | AMD EPYC 7713,225W 135 | AMD EPYC 7663,240W 136 | AMD EPYC 7643,225W 137 | AMD EPYC 7543P,225W 138 | AMD EPYC 7543,225W 139 | AMD EPYC 7513,200W 140 | AMD EPYC 75F3,280W 141 | AMD EPYC 7453,225W 142 | AMD EPYC 7443P,200W 143 | AMD EPYC 7443,200W 144 | AMD EPYC 7413,180W 145 | AMD EPYC 74F3,240W 146 | AMD EPYC 7343,190W 147 | AMD EPYC 7313P,155W 148 | AMD EPYC 7313,155W 149 | AMD EPYC 73F3,240W 150 | AMD EPYC 72F3,180W 151 | AMD EPYC 7742,225W 152 | AMD EPYC 7702P,200W 153 | AMD EPYC 7702,200W 154 | AMD EPYC 7662,225W 155 | AMD EPYC 7H12,280W 156 | AMD EPYC 7642,225W 157 | AMD EPYC 7552,200W 158 | AMD EPYC 7601,180W 159 | AMD EPYC 7551P,180W 160 | AMD EPYC 7551,180W 161 | AMD EPYC 7542,225W 162 | AMD EPYC 7532,200W 163 | AMD EPYC 7502P,180W 164 | AMD EPYC 7502,180W 165 | AMD EPYC 7501,155W 166 | AMD EPYC 7452,155W 167 | AMD EPYC 7451,180W 168 | AMD EPYC 7402P,180W 169 | AMD EPYC 7402,180W 170 | AMD EPYC 7401P,155W 171 | AMD EPYC 7401,155W 172 | AMD EPYC 7352,155W 173 | AMD EPYC 7F72,240W 174 | AMD EPYC 7371,200W 175 | AMD EPYC 7351P,155W 176 | AMD EPYC 7351,155W 177 | AMD EPYC 7302P,155W 178 | AMD EPYC 7302,155W 179 | AMD EPYC 7301,155W 180 | AMD EPYC 7282,120W 181 | AMD EPYC 7281,155W 182 | AMD EPYC 7F52,240W 183 | AMD EPYC 7272,120W 184 | AMD EPYC 7262,155W 185 | AMD EPYC 7261,155W 186 | AMD EPYC 7252,120W 187 | AMD EPYC 7251,120W 188 | AMD EPYC 7232P,120W 189 | AMD EPYC 7F32,180W 190 | 7th Gen FX 9830P APU,35W 191 | 7th Gen FX 9800P APU,15W 192 | FX-9590,220W 193 | FX-9370,220W 194 | FX-8370E,95W 195 | FX-8370,125W 196 | FX-8350,125W 197 | FX-8320E,95W 198 | FX-8320,125W 199 | FX-8310,95W 200 | FX-8300,95W 201 | FX-8150,125W 202 | FX-8120,125W 203 | FX-6350,125W 204 | FX-6300,95W 205 | FX-6200,125W 206 | FX 6100,95W 207 | FX-8800P,15W 208 | FX-7600P,35W 209 | FX-7500,20W 210 | FX-4350,125W 211 | FX-4320,95W 212 | FX-4300,95W 213 | FX-4170,125W 214 | FX-4130,125W 215 | FX-4100,95W 216 | 6th Gen FX-8800P APU,15W 217 | 6th Gen AMD PRO A12-8830B APU,15W 218 | 6th Gen AMD PRO A10-8730B APU,15W 219 | 6th Gen AMD PRO A6-8530B APU,15W 220 | A4 PRO-3350B,15W 221 | 7th Gen AMD PRO A6-8350B APU,15W 222 | 7th Gen AMD PRO A4-5350B APU,15W 223 | 7th Gen AMD PRO A6-7350B APU,15W 224 | 7th Gen AMD PRO A4-4350B APU,15W 225 | 7th Gen AMD PRO A12-9830B APU,35W 226 | 7th Gen AMD PRO A12-9800B APU,15W 227 | 7th Gen AMD PRO A10-9730B APU,35W 228 | 7th Gen AMD PRO A10-9700B APU,15W 229 | 7th Gen AMD PRO A8-9630B,35W 230 | 7th Gen AMD PRO A8-9600B APU,15W 231 | 7th Gen AMD PRO A6-9500B APU,15W 232 | 7th Gen AMD PRO A12-9800E APU,35W 233 | 7th Gen AMD PRO A12-9800 APU,65W 234 | 7th Gen AMD PRO A10-9700E APU,35W 235 | 7th Gen AMD PRO A10-9700 APU,65W 236 | 7th Gen AMD PRO A8-9600 APU,65W 237 | 7th Gen AMD PRO A6-9500E APU,35W 238 | 7th Gen AMD PRO A6-9500 APU,65W 239 | 6th Gen AMD PRO A12-8870 APU,65W 240 | 6th Gen AMD PRO A10-8770E APU,35W 241 | 6th Gen AMD PRO A10-8770 APU,65W 242 | A10 PRO-7850B,95W 243 | A10 PRO-7800B,65W 244 | A10 PRO-7350B,19W 245 | A8 PRO-7600B,65W 246 | A8 PRO-7150B,100W 247 | A4 PRO-3340B,25W 248 | 6th Gen AMD PRO A12-8870E APU,35W 249 | 6th Gen AMD PRO A12-8800B APU,15W 250 | 6th Gen AMD PRO A10-8850B APU,95W 251 | 6th Gen AMD PRO A10-8750B APU,65W 252 | 6th Gen AMD PRO A10-8700B APU,15W 253 | 6th Gen AMD PRO A8-8650B APU,65W 254 | 6th Gen AMD PRO A8-8600B APU,15W 255 | A6 PRO-7400B,65W 256 | A6 PRO-7050B,100W 257 | A4 PRO-7350B,65W 258 | A4 PRO-7300B,65W 259 | 6th Gen AMD PRO A6-8570E APU,35W 260 | 6th Gen AMD PRO A6-8570 APU,65W 261 | 6th Gen AMD PRO A6-8550B APU,65W 262 | 6th Gen AMD PRO A6-8500B APU,15W 263 | 6th Gen AMD PRO A4-8350B APU,65W 264 | AMD Ryzen 7 PRO 2700U,15W 265 | AMD Ryzen 5 PRO 2500U,15W 266 | AMD Ryzen 3 PRO 2300U,15W 267 | AMD Ryzen 9 PRO 3900,65W 268 | AMD Ryzen 7 PRO 3700,65W 269 | AMD Ryzen 5 PRO 3600,65W 270 | AMD Ryzen 5 PRO 3400GE,35W 271 | AMD Ryzen 5 PRO 3400G,65W 272 | AMD Ryzen 3 PRO 3200GE,35W 273 | AMD Ryzen 3 PRO 3200G,65W 274 | AMD Ryzen 7 PRO 4750GE,35W 275 | AMD Ryzen 7 PRO 4750G,65W 276 | AMD Ryzen 5 PRO 4650GE,35W 277 | AMD Ryzen 5 PRO 4650G,65W 278 | AMD Ryzen 5 PRO 3350GE,35W 279 | AMD Ryzen 5 PRO 3350G,65W 280 | AMD Ryzen 3 PRO 4350GE,35W 281 | AMD Ryzen 3 PRO 4350G,65W 282 | AMD Ryzen Threadripper PRO 3995WX,280W 283 | AMD Ryzen Threadripper PRO 3975WX,280W 284 | AMD Ryzen Threadripper PRO 3955WX,280W 285 | AMD Ryzen Threadripper PRO 3945WX,280W 286 | AMD Ryzen 7 PRO 5750GE,35W 287 | AMD Ryzen 7 PRO 5750G,65W 288 | AMD Ryzen 5 PRO 5650GE,35W 289 | AMD Ryzen 5 PRO 5650G,65W 290 | AMD Ryzen 3 PRO 5350GE,35W 291 | AMD Ryzen 3 PRO 5350G,65W 292 | AMD Ryzen 5 PRO 2400GE,35W 293 | AMD Ryzen 5 PRO 2400G,65W 294 | AMD Ryzen 3 PRO 2200GE,35W 295 | AMD Ryzen 3 PRO 2200G,65W 296 | AMD Ryzen 7 PRO 4750U,15W 297 | AMD Ryzen 5 PRO 4650U,15W 298 | AMD Ryzen 3 PRO 4450U,15W 299 | AMD Ryzen 7 PRO 3700U,15W 300 | AMD Ryzen 5 PRO 3500U,15W 301 | AMD Ryzen 3 PRO 3300U,15W 302 | AMD Ryzen 7 PRO 1700X,95W 303 | AMD Ryzen 7 PRO 1700,65W 304 | AMD Ryzen 5 PRO 1600,65W 305 | AMD Ryzen 5 PRO 1500,65W 306 | AMD Ryzen 3 PRO 1300,65W 307 | AMD Ryzen 3 PRO 1200,65W 308 | AMD Ryzen 7 PRO 5850U,15W 309 | AMD Ryzen 5 PRO 5650U,15W 310 | AMD Ryzen 3 PRO 5450U,15W 311 | AMD Ryzen 7 PRO 2700X,95W 312 | AMD Ryzen 7 PRO 2700,65W 313 | AMD Ryzen 5 PRO 2600,65W 314 | AMD Ryzen 7 2700E,45W 315 | AMD Ryzen 5 2600E,45W 316 | AMD Ryzen 5 3450U,15W 317 | AMD Ryzen 7 3750H,35W 318 | AMD Ryzen 7 3700U,15W 319 | AMD Ryzen 5 3550H,35W 320 | AMD Ryzen 5 3500U,15W 321 | AMD Ryzen 3 3350U,15W 322 | AMD Ryzen 3 3300U,15W 323 | AMD Ryzen 3 3200U,15W 324 | AMD Ryzen Threadripper 3970X,280W 325 | AMD Ryzen Threadripper 3960X,280W 326 | AMD Ryzen 9 5950X,105W 327 | AMD Ryzen 9 5900X,105W 328 | AMD Ryzen 7 5800X,105W 329 | AMD Ryzen 5 5600X,65W 330 | AMD Ryzen 7 2700U,15W 331 | AMD Ryzen 5 2500U,15W 332 | AMD Ryzen Threadripper 2970WX,250W 333 | AMD Ryzen Threadripper 2920X,180W 334 | AMD Ryzen 9 3900 Processor ,65W 335 | AMD Ryzen 7 3700C,15W 336 | AMD Ryzen 5 3500C,15W 337 | AMD Ryzen 3 3250C,15W 338 | AMD Ryzen 7 2800H,45W 339 | AMD Ryzen 5 2600H,45W 340 | AMD Ryzen 5 2500X,65W 341 | AMD Ryzen 3 2300X,65W 342 | AMD Ryzen Threadripper 2950X,180W 343 | AMD Ryzen Threadripper 1900X,180W 344 | AMD Ryzen Threadripper 2990WX,250W 345 | AMD Ryzen 9 3900XT,105W 346 | AMD Ryzen 7 3800XT,105W 347 | AMD Ryzen 5 3600XT,95W 348 | AMD Ryzen Threadripper 1950X,180W 349 | AMD Ryzen Threadripper 1920X,180W 350 | AMD Ryzen 7 4700GE ,35W 351 | AMD Ryzen 7 4700G ,65W 352 | AMD Ryzen 5 4600GE ,35W 353 | AMD Ryzen 5 4600G ,65W 354 | AMD Ryzen 3 4300GE ,35W 355 | AMD Ryzen 3 4300G ,65W 356 | AMD Ryzen 9 3900X,105W 357 | AMD Ryzen 7 3800X,105W 358 | AMD Ryzen 7 3700X,65W 359 | AMD Ryzen 5 3600X,95W 360 | AMD Ryzen 5 3600,65W 361 | AMD Ryzen 5 3400G,65W 362 | AMD Ryzen 3 3200G,65W 363 | AMD Ryzen 5 3400GE,35W 364 | AMD Ryzen 3 3200GE,35W 365 | AMD Ryzen 7 5700GE,35W 366 | AMD Ryzen 7 5700G,65W 367 | AMD Ryzen 5 5600GE,35W 368 | AMD Ryzen 5 5600G,65W 369 | AMD Ryzen 3 5300GE,35W 370 | AMD Ryzen 3 5300G,65W 371 | AMD Ryzen 5 1600X,95W 372 | AMD Ryzen 5 1600,65W 373 | AMD Ryzen 5 1500X,65W 374 | AMD Ryzen 5 1400,65W 375 | AMD Ryzen 9 4900H,35W 376 | AMD Ryzen 7 1800X,95W 377 | AMD Ryzen 7 1700X,95W 378 | AMD Ryzen 7 1700,65W 379 | AMD Ryzen Threadripper 3990X,280W 380 | AMD Ryzen 9 5900 ,65W 381 | AMD Ryzen 9 5980HX,45W 382 | AMD Ryzen 9 5980HS,35W 383 | AMD Ryzen 9 5900HX,45W 384 | AMD Ryzen 9 5900HS,35W 385 | AMD Ryzen 7 5800U,15W 386 | AMD Ryzen 7 5800HS,35W 387 | AMD Ryzen 7 5800H,45W 388 | AMD Ryzen 7 5800 ,65W 389 | AMD Ryzen 7 5700U,15W 390 | AMD Ryzen 5 5600U,15W 391 | AMD Ryzen 5 5600HS,35W 392 | AMD Ryzen 5 5600H,45W 393 | AMD Ryzen 5 5500U,15W 394 | AMD Ryzen 3 5400U,15W 395 | AMD Ryzen 3 5300U,15W 396 | AMD Ryzen 3 2300U,15W 397 | AMD Ryzen 3 2200U,15W 398 | AMD Ryzen 7 4800U,15W 399 | AMD Ryzen 7 4800H,45W 400 | AMD Ryzen 7 4700U,15W 401 | AMD Ryzen 5 4600U,15W 402 | AMD Ryzen 5 4600H,45W 403 | AMD Ryzen 5 4500U,15W 404 | AMD Ryzen 3 4300U,15W 405 | AMD Ryzen 3 3250U,15W 406 | AMD Ryzen 3 1300X,65W 407 | AMD Ryzen 3 1200,65W 408 | AMD Ryzen 3 3300X,65W 409 | AMD Ryzen 3 3100,65W 410 | AMD Ryzen 7 2700X,105W 411 | AMD Ryzen 7 2700,65W 412 | AMD Ryzen 5 2600X,95W 413 | AMD Ryzen 5 2600,65W 414 | AMD Ryzen 5 2400GE,35W 415 | AMD Ryzen 3 2200GE,35W 416 | AMD Ryzen 5 2400G,65W 417 | AMD Ryzen 3 2200G,65W 418 | AMD Ryzen 9 3950X,105W 419 | AMD Ryzen 9 4900HS,35W 420 | AMD Ryzen 7 4800HS,45W 421 | AMD Ryzen 5 3500,65W 422 | AMD Ryzen 5 1600 (AF),65W 423 | AMD Ryzen 7 3780U,15W 424 | AMD Ryzen 5 3580U,15W 425 | Sempron 3850 APU,25W 426 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | Using Creative Commons Public Licenses 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 10 | 11 | Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason - for example, because of any applicable exception or limitation to copyright - then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public : wiki.creativecommons.org/Considerations_for_licensees 12 | 13 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | Section 1 - Definitions. 18 | 19 | a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 21 | c. BY-NC-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. 22 | d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 23 | e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 24 | f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 25 | g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. 26 | h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 27 | i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 28 | j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. 29 | k. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 30 | l. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 31 | m. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 32 | n. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 33 | 34 | Section 2 - Scope. 35 | 36 | a. License grant. 37 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 38 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 39 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 40 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 41 | 3. Term. The term of this Public License is specified in Section 6(a). 42 | 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 43 | 5. Downstream recipients. 44 | A. Offer from the Licensor - Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 45 | B. Additional offer from the Licensor - Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. 46 | C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 47 | 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 48 | b. Other rights. 49 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 50 | 2. Patent and trademark rights are not licensed under this Public License. 51 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 52 | 53 | Section 3 - License Conditions. 54 | 55 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 56 | 57 | a. Attribution. 58 | 1. If You Share the Licensed Material (including in modified form), You must: 59 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 60 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 61 | ii. a copyright notice; 62 | iii. a notice that refers to this Public License; 63 | iv. a notice that refers to the disclaimer of warranties; 64 | 65 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 66 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 67 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 68 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 69 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 70 | b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 71 | 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. 72 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 73 | 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 74 | 75 | Section 4 - Sui Generis Database Rights. 76 | 77 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 78 | 79 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 80 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and 81 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 82 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 83 | 84 | Section 5 - Disclaimer of Warranties and Limitation of Liability. 85 | 86 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 87 | b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 88 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 89 | 90 | Section 6 - Term and Termination. 91 | 92 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 93 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 94 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 95 | 2. upon express reinstatement by the Licensor. 96 | 97 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 98 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 99 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 100 | 101 | Section 7 - Other Terms and Conditions. 102 | 103 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 104 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 105 | 106 | Section 8 - Interpretation. 107 | 108 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 109 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 110 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 111 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 112 | 113 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the "Licensor." The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 114 | 115 | Creative Commons may be contacted at creativecommons.org. 116 | -------------------------------------------------------------------------------- /ecofreq/ecofreq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from datetime import datetime 5 | import configparser 6 | import argparse 7 | import heapq 8 | import traceback 9 | import copy 10 | import getpass 11 | 12 | import ecofreq.helpers as efh 13 | 14 | from ecofreq import __version__ 15 | from .mqtt import * 16 | from ecofreq.config import * 17 | from ecofreq.utils import * 18 | from ecofreq.ipc import EcoServer 19 | from ecofreq.monitors.manager import MonitorManager 20 | from ecofreq.providers.manager import EcoProviderManager, EcoProvider 21 | from ecofreq.policy.manager import EcoPolicyManager 22 | from ecofreq.policy.idle import IdlePolicy 23 | from ecofreq.install import EcofreqInstaller 24 | 25 | class EcoFreqController(object): 26 | 27 | def __init__(self, ef): 28 | self.ef = ef 29 | 30 | def run_cmd(self, cmd, args={}): 31 | res = {} 32 | try: 33 | if hasattr(self, cmd): 34 | getattr(self, cmd)(res, args) 35 | res['status'] = 'OK' 36 | else: 37 | res['status'] = 'ERROR' 38 | res['error'] = 'Unknown command: ' + cmd 39 | except: 40 | res['status'] = 'ERROR' 41 | res['error'] = 'Exception: ' + sys.exc_info() 42 | 43 | return res 44 | 45 | def info(self, res, args): 46 | res.update(self.ef.get_info()) 47 | m_stats = self.ef.monitor.get_stats() 48 | if "LastState" in m_stats: 49 | res['idle_state'] = m_stats["LastState"] 50 | res['idle_load'] = m_stats["LastLoad"] 51 | res['idle_duration'] = m_stats["IdleDuration"] 52 | else: 53 | res['idle_state'] = "NA" 54 | res['avg_power'] = self.ef.monitor.get_last_avg_power() 55 | res['avg_freq'] = self.ef.monitor.get_last_cpu_avg_freq() 56 | res['total_energy_j'] = self.ef.monitor.get_total_energy() 57 | res['total_co2'] = self.ef.total_co2 58 | res['total_cost'] = self.ef.total_cost 59 | res['last_co2kwh'] = self.ef.last_co2kwh 60 | res['last_price'] = self.ef.last_price 61 | 62 | def get_policy(self, res, args): 63 | res['co2policy'] = self.ef.co2policy.get_config() 64 | 65 | def set_policy(self, res, args): 66 | new_cfg = {} 67 | for domain in args["co2policy"].keys(): 68 | dpol = domain + "_policy" 69 | if dpol in self.ef.config: 70 | old_cfg = dict(self.ef.config[dpol]) 71 | else: 72 | old_cfg = dict(self.ef.config["policy"]) 73 | # print(old_cfg) 74 | new_cfg[domain] = copy.deepcopy(old_cfg) 75 | new_cfg[domain].update(args["co2policy"][domain]) 76 | # all domains use the same metric for now 77 | new_cfg["metric"] = args["co2policy"][domain]["metric"] 78 | # print(new_cfg) 79 | self.ef.co2policy.set_config(new_cfg) 80 | if self.ef.last_co2_data: 81 | self.ef.co2policy.set_co2(self.ef.last_co2_data) 82 | self.ef.co2logger.print_cmd("set_policy") 83 | 84 | def get_provider(self, res, args): 85 | res['co2provider'] = self.ef.co2provider.get_config() 86 | 87 | def set_provider(self, res, args): 88 | old_cfg = self.ef.config 89 | # print(args["co2provider"]) 90 | new_cfg = copy.deepcopy(old_cfg) 91 | try: 92 | new_cfg.read_dict(args["co2provider"]) 93 | self.ef.reset_co2provider(new_cfg) 94 | except: 95 | print(sys.exc_info()) 96 | 97 | class CO2History(object): 98 | def __init__(self, config): 99 | self.config = config 100 | self.h = [] 101 | 102 | def add_co2(self, co2): 103 | heapq.heappush(self.h, co2) 104 | 105 | def min_co2(self, quantile = 5): 106 | n = int(0.01 * quantile * len(self.h)) + 1 107 | return heapq.nsmallest(n, self.h)[n-1] 108 | 109 | def max_co2(self, quantile = 5): 110 | n = int(0.01 * quantile * len(self.h)) + 1 111 | return heapq.nlargest(n, self.h)[n-1] 112 | 113 | class MQTTLogger(object): 114 | def __init__(self, config, iface): 115 | self.iface = iface 116 | self.label = "mqtt_logger" 117 | cfg = config[self.label] 118 | self.mqtt_client = MQTTManager.add_client(self.label, cfg) 119 | 120 | def log(self): 121 | data = self.iface.run_cmd("info") 122 | self.mqtt_client.put_msg(data) 123 | 124 | class EcoLogger(object): 125 | def __init__(self, config): 126 | self.log_fname = config["general"]["logfile"] 127 | if self.log_fname in OPTION_DISABLED: 128 | self.log_fname = None 129 | self.fmt = NAFormatter() 130 | self.idle_fields = False 131 | self.idle_debug = False 132 | self.cost_fields = config["general"].get("logcost", True) 133 | self.co2_extra = config["general"].get("logco2extra", False) 134 | 135 | def init_fields(self, monitors): 136 | if monitors.get_period_idle(): 137 | self.idle_fields = True 138 | # self.idle_debug = True 139 | self.row_fmt = '{:<20}\t{:>10}\t{:>10}\t{:>10}\t{:>12.3f}\t{:>12.3f}\t{:>12.3f}\t{:>10.3f}\t{:>10.3f}' 140 | if self.idle_fields: 141 | self.row_fmt += "\t{:<7}" 142 | if self.idle_debug: 143 | self.row_fmt += "\t{:>10}\t{:>10.3f}" 144 | if self.co2_extra: 145 | self.row_fmt += "\t{:>10}\t{:>8.3f}\t{:>10}" 146 | if self.cost_fields: 147 | self.row_fmt += "\t{:>8.3f}\t{:>8.3f}" 148 | 149 | self.header_fmt = "#" + self.row_fmt.replace(".3f", "") 150 | 151 | def log(self, logstr): 152 | print (logstr) 153 | if self.log_fname: 154 | with open(self.log_fname, "a") as logf: 155 | logf.write(logstr + "\n") 156 | 157 | def print_header(self): 158 | headers = ["Timestamp", "gCO2/kWh", "Fmax [Mhz]", "Favg [Mhz]", "CPU_Pmax [W]", "GPU_Pmax [W]", "SYS_Pavg [W]", "Energy [J]", "CO2 [g]"] 159 | if self.idle_fields: 160 | headers += ["State"] 161 | if self.idle_debug: 162 | headers += ["MaxSessions", "MaxLoad"] 163 | if self.co2_extra: 164 | headers += ["CI [g/kWh]", "Fossil [%]", "Index"] 165 | if self.cost_fields: 166 | headers += ["Price/kWh", "Cost"] 167 | self.log(self.fmt.format(self.header_fmt, *headers)) 168 | 169 | def print_row(self, co2kwh, period_price, avg_freq, energy, avg_power, co2period, period_cost, idle, stats, co2_data): 170 | ts = datetime.now().strftime(TS_FORMAT) 171 | max_freq = cpu_max_power = gpu_max_power = None 172 | if efh.CpuFreqHelper.available(): 173 | max_freq = round(efh.CpuFreqHelper.get_gov_max_freq(unit=efh.CpuFreqHelper.MHZ)) 174 | if efh.LinuxPowercapHelper.available(): 175 | cpu_max_power = efh.LinuxPowercapHelper.get_power_limit(efh.LinuxPowercapHelper.WATT) 176 | elif efh.AMDEsmiHelper.available(): 177 | cpu_max_power = efh.AMDEsmiHelper.get_power_limit(efh.AMDEsmiHelper.WATT) 178 | if efh.NvidiaGPUHelper.available(): 179 | gpu_max_power = efh.NvidiaGPUHelper.get_power_limit() 180 | cols = [ts, safe_round(co2kwh), max_freq, safe_round(avg_freq), cpu_max_power, gpu_max_power, avg_power, energy, co2period] 181 | if self.idle_fields: 182 | cols += [idle] 183 | if self.idle_debug: 184 | cols += [stats["MaxSessions"], stats["MaxLoad"]] 185 | if self.co2_extra: 186 | cols += [safe_round(co2_data.get(EcoProvider.FIELD_CO2)), co2_data.get(EcoProvider.FIELD_FOSSIL_PCT), co2_data.get(EcoProvider.FIELD_INDEX)] 187 | if self.cost_fields: 188 | cols += [period_price, period_cost] 189 | 190 | logstr = self.fmt.format(self.row_fmt, *cols) 191 | 192 | # logstr += "\t" + str(self.co2history.min_co2()) + "\t" + str(self.co2history.max_co2()) 193 | 194 | self.log(logstr) 195 | 196 | def print_cmd(self, cmd): 197 | ts = datetime.now().strftime(TS_FORMAT) 198 | logstr = "##" + ts + "\t" + cmd.upper() 199 | self.log(logstr) 200 | 201 | class EcoFreq(object): 202 | def __init__(self, config): 203 | self.config = config 204 | 205 | self.co2provider = EcoProviderManager(config) 206 | self.co2policy = EcoPolicyManager(config) 207 | self.co2history = CO2History(config) 208 | self.co2logger = EcoLogger(config) 209 | self.monitor = MonitorManager(config) 210 | self.co2logger.init_fields(self.monitor) 211 | self.debug = False 212 | self.idle_policy = IdlePolicy.from_config(config) 213 | if self.idle_policy: 214 | self.idle_policy.init_monitors(self.monitor) 215 | self.idle_policy.init_logger(self.co2logger) 216 | 217 | self.iface = EcoFreqController(self) 218 | self.server = EcoServer(self.iface, config) 219 | 220 | mqtt_log = config["general"].get("logmqtt", False) 221 | if mqtt_log: 222 | self.mqtt_logger = MQTTLogger(config, self.iface) 223 | else: 224 | self.mqtt_logger = None 225 | 226 | # make sure that CO2 sampling interval is a multiple of energy sampling interval 227 | self.sample_interval = self.monitor.adjust_interval(self.co2provider.interval) 228 | # print("sampling intervals co2/energy:", self.co2provider.interval, self.sample_interval) 229 | self.last_co2_data = self.co2provider.get_data() 230 | self.last_co2kwh = self.last_co2_data.get(EcoProvider.FIELD_CO2, None) 231 | self.last_price = self.last_co2_data.get(EcoProvider.FIELD_PRICE, None) 232 | #self.last_co2_data = self.last_co2kwh = self.last_price = None 233 | self.total_co2 = 0. 234 | self.total_cost = 0. 235 | self.start_date = datetime.now() 236 | self.co2provider_updated = False 237 | 238 | def get_info(self): 239 | return {"logfile": self.co2logger.log_fname, 240 | "co2provider": self.co2provider.info_string(), 241 | "co2policy": self.co2policy.info_string(), 242 | "idlepolicy": self.idle_policy.info_string() if self.idle_policy else "None", 243 | "monitors": self.monitor.info_string(), 244 | "start_date": self.start_date.strftime(TS_FORMAT) } 245 | 246 | @classmethod 247 | def print_info(cls, info): 248 | print("Log file: ", info["logfile"]) 249 | print("CO2 Provider:", info["co2provider"]) 250 | print("CO2 Policy: ", info["co2policy"]) 251 | print("Idle Policy: ", info["idlepolicy"]) 252 | print("Monitors: ", info["monitors"]) 253 | 254 | def info(self): 255 | info = self.get_info() 256 | EcoFreq.print_info(info) 257 | 258 | def reset_co2provider(self, cfg): 259 | self.co2provider = EcoProviderManager(cfg) 260 | self.co2provider_updated = True 261 | self.co2logger.print_cmd("set_provider") 262 | 263 | def update_co2(self): 264 | # fetch new co2 intensity 265 | co2_data = self.co2provider.get_data() 266 | co2 = co2_data.get(EcoProvider.FIELD_CO2, None) 267 | if co2: 268 | if self.last_co2kwh: 269 | self.period_co2kwh = 0.5 * (co2 + self.last_co2kwh) 270 | else: 271 | self.period_co2kwh = co2 272 | else: 273 | self.period_co2kwh = self.last_co2kwh 274 | self.last_co2kwh = co2 275 | 276 | price_kwh = co2_data.get(EcoProvider.FIELD_PRICE, None) 277 | if price_kwh: 278 | if self.last_price: 279 | self.period_price = self.last_price 280 | else: 281 | self.period_price = price_kwh 282 | else: 283 | self.period_price = self.last_price 284 | 285 | # prepare and print log row -> shows values for *past* interval! 286 | idle = self.monitor.get_period_idle() 287 | avg_freq = self.monitor.get_period_cpu_avg_freq(efh.CpuFreqHelper.MHZ) 288 | energy = self.monitor.get_period_energy() 289 | avg_power = self.monitor.get_period_avg_power() 290 | if self.period_co2kwh: 291 | period_co2 = energy * self.period_co2kwh / JOULES_IN_KWH 292 | self.total_co2 += period_co2 293 | else: 294 | period_co2 = None 295 | 296 | if self.period_price: 297 | period_cost = energy * self.period_price / JOULES_IN_KWH 298 | self.total_cost += period_cost 299 | else: 300 | period_cost = None 301 | 302 | stats = self.monitor.get_stats() 303 | 304 | self.co2logger.print_row(self.period_co2kwh, self.period_price, avg_freq, energy, avg_power, period_co2, period_cost, idle, stats, co2_data) 305 | 306 | # apply policy for new co2 reading 307 | self.co2policy.set_co2(co2_data) 308 | 309 | if co2: 310 | self.co2history.add_co2(co2) 311 | 312 | self.last_co2_data = co2_data 313 | self.last_co2kwh = co2 314 | self.last_price = price_kwh 315 | 316 | def write_shm(self): 317 | ts = datetime.now().timestamp() 318 | energy_j = str(round(self.monitor.get_total_energy(), 3)) 319 | co2_g = self.total_co2 320 | cost = self.total_cost 321 | period_energy = self.monitor.get_period_energy() 322 | if period_energy > 0.: 323 | if self.last_co2kwh: 324 | co2_g += period_energy * self.last_co2kwh / JOULES_IN_KWH 325 | if self.last_price: 326 | cost += period_energy * self.last_price / JOULES_IN_KWH 327 | 328 | ts = str(round(ts)) 329 | co2_g = str(round(co2_g, 3)) 330 | cost = str(round(cost, 3)) 331 | with open(SHM_FILE, "w") as f: 332 | f.write(" ".join([ts, energy_j, co2_g, cost])) 333 | 334 | def write_mqtt(self): 335 | if self.mqtt_logger: 336 | self.mqtt_logger.log() 337 | 338 | async def spin(self): 339 | try: 340 | self.co2logger.print_header() 341 | self.co2logger.print_cmd("start") 342 | duration = 0 343 | self.monitor.reset_period() 344 | elapsed = 0 345 | while 1: 346 | to_sleep = max(self.sample_interval - elapsed, 0) 347 | #print("to_sleep:", to_sleep) 348 | # time.sleep(to_sleep) 349 | await asyncio.sleep(to_sleep) 350 | duration += self.sample_interval 351 | t1 = datetime.now() 352 | self.monitor.update(duration) 353 | do_update_co2 = duration % self.co2provider.interval == 0 354 | if self.co2provider_updated: 355 | do_update_co2 = True 356 | self.co2provider_updated = False 357 | if do_update_co2: 358 | self.update_co2() 359 | self.monitor.reset_period() 360 | self.write_shm() 361 | self.write_mqtt() 362 | if self.idle_policy: 363 | if self.idle_policy.check_idle(): 364 | self.monitor.update(0) 365 | self.monitor.reset_period() 366 | self.co2logger.print_cmd("wakeup") 367 | t1 = datetime.now() 368 | elapsed = (datetime.now() - t1).total_seconds() 369 | # print("elapsed:", elapsed) 370 | except: 371 | e = sys.exc_info() 372 | print ("Exception: ", e) 373 | self.co2policy.reset() 374 | 375 | async def main(self): 376 | spins = [self.server.spin(), MQTTManager.run(), self.spin()] 377 | tasks = [asyncio.create_task(t) for t in spins] 378 | for t in tasks: 379 | await t 380 | 381 | def cmd_install(args): 382 | EcofreqInstaller.install(args) 383 | 384 | def cmd_remove(args): 385 | EcofreqInstaller.uninstall(args) 386 | 387 | def cmd_info(args): 388 | print_sysinfo() 389 | 390 | def cmd_showcfg(args): 391 | parser = read_config(args) 392 | parser.write(sys.stdout) 393 | 394 | def parse_args(): 395 | parser = argparse.ArgumentParser() 396 | parser.add_argument("-c", dest="cfg_file", default=None, help="Config file name.") 397 | parser.add_argument("-g", dest="governor", default=None, help="Power governor (off = no power scaling).") 398 | parser.add_argument("-l", dest="log_fname", default=None, help="Log file name.") 399 | parser.add_argument("-t", dest="co2token", default=None, help="CO2Signal token.") 400 | parser.add_argument("-i", dest="interval", default=None, help="Provider polling interval in seconds.") 401 | parser.add_argument("--user", dest="usermode", action="store_true", 402 | help="Run in rootless mode (limited functionality)") 403 | 404 | subparsers = parser.add_subparsers(dest="subcommand") 405 | install_parser = subparsers.add_parser( 406 | "install", help="Install EcoFreq systemd service and configure permissions." 407 | ) 408 | try: 409 | real_login = os.getlogin() 410 | except: 411 | real_login = None 412 | install_parser.add_argument("-u", dest="duser", default=real_login, 413 | help="User to run EcoFreq daemon") 414 | install_parser.add_argument("-g", dest="dgroup", default="ecofreq", 415 | help="User group for EcoFreq daemon (default: ecofreq)") 416 | install_parser.set_defaults(func=cmd_install) 417 | 418 | remove_parser = subparsers.add_parser( 419 | "remove", help="Remove EcoFreq systemd service." 420 | ) 421 | remove_parser.set_defaults(func=cmd_remove) 422 | 423 | info_parser = subparsers.add_parser( 424 | "info", help="Show system info and exit." 425 | ) 426 | info_parser.add_argument("--sudo", dest="usermode", action="store_false", 427 | help="Run in rootful mode (elevate with sudo if needed)") 428 | info_parser.set_defaults(func=cmd_info, usermode=True) 429 | 430 | showcfg_parser = subparsers.add_parser( 431 | "showcfg", help="Print EcoFreq configuration." 432 | ) 433 | showcfg_parser.set_defaults(func=cmd_showcfg, usermode=True) 434 | 435 | args = parser.parse_args() 436 | return args 437 | 438 | def read_config(args): 439 | def_dict = {'general' : { 'LogFile' : LOG_FILE }, 440 | 'provider' : { 'Interval' : '600' }, 441 | 'policy' : { 'Control' : 'auto', 442 | 'Governor' : 'linear', 443 | 'CO2Range' : 'auto' }, 444 | 'monitor' : { 'PowerSensor' : 'auto', 445 | 'Interval' : '5' } 446 | } 447 | 448 | if args and args.cfg_file: 449 | cfg_file = args.cfg_file 450 | else: 451 | cfg_file = os.path.join(CONFIGDIR, "default.cfg") 452 | 453 | if not os.path.exists(cfg_file): 454 | # one of the built-in profiles? 455 | cfg_file = os.path.join(CONFIGDIR, f"{cfg_file}.cfg") 456 | if not os.path.exists(cfg_file): 457 | print("ERROR: Config file not found: ", cfg_file) 458 | sys.exit(-1) 459 | 460 | parser = configparser.ConfigParser(allow_no_value=True) 461 | parser.read_dict(def_dict) 462 | parser.read(cfg_file) 463 | 464 | if args: 465 | if args.co2token: 466 | if "all" in parser["provider"]: 467 | prov = parser["provider"]["all"] 468 | parser[prov]["token"] = args.co2token 469 | if args.log_fname: 470 | parser["general"]["LogFile"] = args.log_fname 471 | if args.governor: 472 | parser["policy"]["Governor"] = args.governor 473 | if args.interval: 474 | parser["provider"]["interval"] = args.interval 475 | 476 | return parser 477 | 478 | def print_sysinfo(): 479 | print(f"EcoFreq v{__version__} (c) 2025 Oleksiy Kozlov\n") 480 | efh.CpuInfoHelper.info() 481 | print("") 482 | efh.LinuxPowercapHelper.info() 483 | print("") 484 | efh.AMDEsmiHelper.info() 485 | print("") 486 | efh.CpuFreqHelper.info() 487 | print("") 488 | efh.NvidiaGPUHelper.info() 489 | print("") 490 | efh.IPMIHelper.info() 491 | print("") 492 | efh.LinuxCgroupHelper.info() 493 | print("") 494 | efh.SuspendHelper.info() 495 | print("") 496 | 497 | def main(): 498 | args = parse_args() 499 | 500 | if os.getuid() != 0 and not args.usermode: 501 | print("\nTrying to obtain root permissions, please enter your password if requested...") 502 | from elevate import elevate 503 | elevate(graphical=False) 504 | 505 | # handle special commands: install, info etc. 506 | if args.subcommand: 507 | if hasattr(args, "func"): 508 | args.func(args) 509 | return 510 | 511 | # normal startup 512 | try: 513 | print_sysinfo() 514 | 515 | cfg = read_config(args) 516 | ef = EcoFreq(cfg) 517 | ef.info() 518 | print("") 519 | asyncio.run(ef.main()) 520 | except PermissionError: 521 | print(traceback.format_exc()) 522 | print("\nPlease run EcoFreq with root permissions!\n") 523 | except SystemExit: 524 | pass 525 | except: 526 | print("Exception:", traceback.format_exc()) 527 | 528 | if os.path.exists(SHM_FILE): 529 | os.remove(SHM_FILE) 530 | 531 | if __name__ == '__main__': 532 | main() 533 | --------------------------------------------------------------------------------