log info and statistics of every trigger
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
<logLevel>Info</logLevel>
|
<logLevel>Info</logLevel>
|
||||||
<testMode>true</testMode>
|
<testMode>true</testMode>
|
||||||
<defaultGovernor>powersave</defaultGovernor>
|
<defaultGovernor>powersave</defaultGovernor>
|
||||||
|
<statisticsIntervalInSeconds>5</statisticsIntervalInSeconds>
|
||||||
<triggers>
|
<triggers>
|
||||||
<trigger>
|
<trigger>
|
||||||
<name>test_trigger1</name>
|
<name>test_trigger1</name>
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
<trigger>
|
<trigger>
|
||||||
<name>roon</name>
|
<name>roon</name>
|
||||||
<logLevel>Info</logLevel>
|
<logLevel>Info</logLevel>
|
||||||
<intervalInSeconds>2</intervalInSeconds>
|
<intervalInSeconds>1</intervalInSeconds>
|
||||||
<governor>performance</governor>
|
<governor>performance</governor>
|
||||||
<customConfig>
|
<customConfig>
|
||||||
<extensionId>cpu_governor_auto_adjust_for_roon</extensionId>
|
<extensionId>cpu_governor_auto_adjust_for_roon</extensionId>
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
<trigger>
|
<trigger>
|
||||||
<name>cpu_load</name>
|
<name>cpu_load</name>
|
||||||
<logLevel>Debug</logLevel>
|
<logLevel>Debug</logLevel>
|
||||||
<intervalInSeconds>5</intervalInSeconds>
|
<intervalInSeconds>1</intervalInSeconds>
|
||||||
<governor>performance</governor>
|
<governor>performance</governor>
|
||||||
<customConfig>
|
<customConfig>
|
||||||
<oneMinuteHighThreshold>0.35</oneMinuteHighThreshold>
|
<oneMinuteHighThreshold>0.35</oneMinuteHighThreshold>
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
<trigger>
|
<trigger>
|
||||||
<name>roon_arc</name>
|
<name>roon_arc</name>
|
||||||
<logLevel>Info</logLevel>
|
<logLevel>Info</logLevel>
|
||||||
<intervalInSeconds>5</intervalInSeconds>
|
<intervalInSeconds>1</intervalInSeconds>
|
||||||
<governor>performance</governor>
|
<governor>performance</governor>
|
||||||
<customConfig>
|
<customConfig>
|
||||||
<logFile>/home/martin/dev/roon_arc/RoonServer_log.txt</logFile>
|
<logFile>/home/martin/dev/roon_arc/RoonServer_log.txt</logFile>
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ class Config:
|
|||||||
raise ValueError("testMode can only be 'true' or 'false")
|
raise ValueError("testMode can only be 'true' or 'false")
|
||||||
return True if value == 'true' else False
|
return True if value == 'true' else False
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def statistics_interval_in_seconds(self) -> int:
|
||||||
|
_statistics_interval = self._get_single_text_value_from_xpath('statisticsIntervalInSeconds')
|
||||||
|
value = int(_statistics_interval)
|
||||||
|
if value < 1:
|
||||||
|
raise ValueError("statisticsIntervalInSeconds must be greater than 0")
|
||||||
|
return value
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def default_governor(self) -> str:
|
def default_governor(self) -> str:
|
||||||
_default_governor = self._get_single_text_value_from_xpath('defaultGovernor')
|
_default_governor = self._get_single_text_value_from_xpath('defaultGovernor')
|
||||||
|
|||||||
@@ -42,16 +42,9 @@ class TriggerScheduler(AppClass):
|
|||||||
|
|
||||||
self.governor_control.set_governor(governor_name)
|
self.governor_control.set_governor(governor_name)
|
||||||
|
|
||||||
async def async_trigger(self, _trigger: Trigger) -> None:
|
|
||||||
"""Run a callback_trigger with a specific name and check its status at a given interval."""
|
|
||||||
await _trigger.async_run()
|
|
||||||
while True:
|
|
||||||
self.log.debug("callback trigger %s, state: %s", _trigger.name, _trigger.trigger_state)
|
|
||||||
await asyncio.sleep(_trigger.config.interval_in_seconds)
|
|
||||||
|
|
||||||
async def start_trigger(self, _trigger: Trigger) -> None:
|
async def start_trigger(self, _trigger: Trigger) -> None:
|
||||||
"""Start a new trigger."""
|
"""Start a new trigger."""
|
||||||
self.loop.create_task(self.async_trigger(_trigger))
|
self.loop.create_task(_trigger.run())
|
||||||
self.running_triggers.append(_trigger)
|
self.running_triggers.append(_trigger)
|
||||||
|
|
||||||
async def stop_triggers(self) -> None:
|
async def stop_triggers(self) -> None:
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ from cpu_governor_auto_adjust.config import Config, TriggerTuple
|
|||||||
from cpu_governor_auto_adjust.exceptions import MissingConfig, TriggerImportError
|
from cpu_governor_auto_adjust.exceptions import MissingConfig, TriggerImportError
|
||||||
from cpu_governor_auto_adjust.governor import Governor, _governor_list
|
from cpu_governor_auto_adjust.governor import Governor, _governor_list
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from time import monotonic
|
||||||
|
from datetime import datetime
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
class Trigger(AppClass):
|
class Trigger(AppClass):
|
||||||
@@ -10,10 +13,21 @@ class Trigger(AppClass):
|
|||||||
super().__init__(_config)
|
super().__init__(_config)
|
||||||
self.active: bool = False
|
self.active: bool = False
|
||||||
self.log.setLevel(self.config.loglevel.upper())
|
self.log.setLevel(self.config.loglevel.upper())
|
||||||
|
self._timestamp_last_statistics = monotonic()
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return super().__hash__(hash(self.name))
|
return super().__hash__(hash(self.name))
|
||||||
|
|
||||||
|
def log_statistics(self):
|
||||||
|
instance_vars = {}
|
||||||
|
|
||||||
|
for key, value in self.__dict__.items():
|
||||||
|
if not key.startswith("_") and isinstance(value, (str, int, float, bool, list, dict, datetime, Governor)):
|
||||||
|
if not isinstance(value, (int, float, Governor)):
|
||||||
|
value = str(value)
|
||||||
|
instance_vars[key] = value
|
||||||
|
self.log.info("info and statistics: %s", instance_vars)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trigger_state(self) -> str:
|
def trigger_state(self) -> str:
|
||||||
return "active" if self.active else "not active"
|
return "active" if self.active else "not active"
|
||||||
@@ -41,18 +55,20 @@ class Trigger(AppClass):
|
|||||||
raise ValueError("unknown governor: %s", _governor)
|
raise ValueError("unknown governor: %s", _governor)
|
||||||
return _governor
|
return _governor
|
||||||
|
|
||||||
def run(self) -> None:
|
def trigger_code(self) -> None:
|
||||||
# this is the main function of the trigger class and has to stay active by using a while True loop
|
|
||||||
|
|
||||||
while True:
|
|
||||||
raise TriggerImportError(
|
|
||||||
"the Trigger class can't used directly, but must be inherited in a trigger specific class"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_run(self) -> None:
|
|
||||||
# this is the main function of the trigger class and can be used to execute a callback function
|
# this is the main function of the trigger class and can be used to execute a callback function
|
||||||
|
|
||||||
|
raise TriggerImportError(
|
||||||
|
"the Trigger class can't used directly, but must be inherited in a trigger specific class"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
raise TriggerImportError(
|
self.log.debug("run trigger code of %s", self.__class__.__name__)
|
||||||
"the Trigger class can't used directly, but must be inherited in a trigger specific class"
|
if self._timestamp_last_statistics + int(self._config.statistics_interval_in_seconds) < monotonic():
|
||||||
)
|
self.log_statistics()
|
||||||
|
self._timestamp_last_statistics = monotonic()
|
||||||
|
self.trigger_code()
|
||||||
|
self.log.debug("trigger state: %s", self.trigger_state)
|
||||||
|
await asyncio.sleep(self.config.interval_in_seconds)
|
||||||
|
|
||||||
@@ -2,7 +2,6 @@ from cpu_governor_auto_adjust.trigger import Trigger
|
|||||||
from cpu_governor_auto_adjust.config import Config
|
from cpu_governor_auto_adjust.config import Config
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from os import getloadavg
|
from os import getloadavg
|
||||||
import asyncio
|
|
||||||
|
|
||||||
|
|
||||||
class CpuLoadTrigger(Trigger):
|
class CpuLoadTrigger(Trigger):
|
||||||
@@ -55,34 +54,29 @@ class CpuLoadTrigger(Trigger):
|
|||||||
def current_load_average_over_under_threshold(self) -> bool:
|
def current_load_average_over_under_threshold(self) -> bool:
|
||||||
return any(load <= threshold for load, threshold in zip(self.current_load, self.low_threshold))
|
return any(load <= threshold for load, threshold in zip(self.current_load, self.low_threshold))
|
||||||
|
|
||||||
async def check_load(self) -> None:
|
def trigger_code(self) -> None:
|
||||||
while True:
|
current_active = self.active
|
||||||
current_active = self.active
|
new_high_load_active = self.current_load_average_over_high_threshold
|
||||||
new_high_load_active = self.current_load_average_over_high_threshold
|
new_low_load_active = self.current_load_average_over_under_threshold
|
||||||
new_low_load_active = self.current_load_average_over_under_threshold
|
|
||||||
|
|
||||||
if not current_active and new_high_load_active:
|
if not current_active and new_high_load_active:
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"activating trigger, load: %s, high threshold: %s, governor: %s",
|
"activating trigger, load: %s, high threshold: %s, governor: %s",
|
||||||
self.current_load, self.high_threshold, self.governor.name
|
self.current_load, self.high_threshold, self.governor.name
|
||||||
)
|
)
|
||||||
self.active = True
|
self.active = True
|
||||||
|
|
||||||
elif current_active and new_high_load_active:
|
elif current_active and new_high_load_active:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"trigger is already active, load: %s, high threshold: %s",
|
"trigger is already active, load: %s, high threshold: %s",
|
||||||
self.current_load, self.high_threshold
|
self.current_load, self.high_threshold
|
||||||
)
|
)
|
||||||
|
|
||||||
elif current_active and new_low_load_active:
|
elif current_active and new_low_load_active:
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"deactivating trigger, load: %s, low threshold: %s, governor: %s",
|
"deactivating trigger, load: %s, low threshold: %s, governor: %s",
|
||||||
self.current_load, self.low_threshold, self.governor.name
|
self.current_load, self.low_threshold, self.governor.name
|
||||||
)
|
)
|
||||||
self.active = False
|
self.active = False
|
||||||
else:
|
else:
|
||||||
self.log.debug("trigger is already inactive, load: %s, threshold: %s", self.current_load, self.high_threshold)
|
self.log.debug("trigger is already inactive, load: %s, threshold: %s", self.current_load, self.high_threshold)
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
async def async_run(self) -> None:
|
|
||||||
await self.check_load()
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from cpu_governor_auto_adjust.config import Config
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import asyncio
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +47,8 @@ class LogTrigger(Trigger):
|
|||||||
super().__init__(_config)
|
super().__init__(_config)
|
||||||
self.timestamp_last_active_change = datetime.now() - timedelta(minutes=self.timeout_in_minutes + 1)
|
self.timestamp_last_active_change = datetime.now() - timedelta(minutes=self.timeout_in_minutes + 1)
|
||||||
self.line_count = 0
|
self.line_count = 0
|
||||||
|
self._file = None
|
||||||
|
self._current_inode = None
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def file(self) -> str:
|
def file(self) -> str:
|
||||||
@@ -76,14 +77,11 @@ class LogTrigger(Trigger):
|
|||||||
if not self.active:
|
if not self.active:
|
||||||
self.log.info("activating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
|
self.log.info("activating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
|
||||||
self.active = True
|
self.active = True
|
||||||
self.log.info("Statistics: %s", self.get_statistic_info())
|
|
||||||
|
|
||||||
|
|
||||||
def set_inactive(self) -> None:
|
def set_inactive(self) -> None:
|
||||||
if self.active:
|
if self.active:
|
||||||
self.log.info("deactivating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
|
self.log.info("deactivating trigger, log file: %s, trigger strings: %s, governor: %s", self.file, self.trigger_strings, self.governor.name)
|
||||||
self.active = False
|
self.active = False
|
||||||
self.log.info("Statistics: %s", self.get_statistic_info())
|
|
||||||
|
|
||||||
def _process_line(self, line: str) -> None:
|
def _process_line(self, line: str) -> None:
|
||||||
for trigger_string in self.trigger_strings:
|
for trigger_string in self.trigger_strings:
|
||||||
@@ -106,45 +104,30 @@ class LogTrigger(Trigger):
|
|||||||
self.log.debug("substrings condition not met")
|
self.log.debug("substrings condition not met")
|
||||||
self.log.debug("No trigger strings matched in line: %s", line.strip())
|
self.log.debug("No trigger strings matched in line: %s", line.strip())
|
||||||
|
|
||||||
def get_statistic_info(self) -> dict[str, str]:
|
def trigger_code(self) -> None:
|
||||||
return {
|
try:
|
||||||
'file': self.file,
|
if self._file is None:
|
||||||
'active': str(self.active),
|
self.log.info("Opening file: %s", self.file)
|
||||||
'line_count': str(self.line_count),
|
self._file = open(self.file, 'r')
|
||||||
'timestamp_last_active_change': str(self.timestamp_last_active_change),
|
self._current_inode = os.stat(self.file).st_ino
|
||||||
'timeout_in_minutes': str(self.timeout_in_minutes),
|
self._file.seek(0, 2) # Move to the end of the file
|
||||||
}
|
|
||||||
|
|
||||||
async def read_log(self) -> None:
|
# Detect if file has been rotated
|
||||||
while True:
|
if os.stat(self.file).st_ino != self._current_inode:
|
||||||
try:
|
self.log.info("File %s rolled over. Reopening...", self.file)
|
||||||
current_inode = os.stat(self.file).st_ino # Get initial inode
|
self._file.close()
|
||||||
|
self._file = open(self.file, 'r')
|
||||||
|
self._current_inode = os.stat(self.file).st_ino
|
||||||
|
|
||||||
with open(self.file, 'r') as _file:
|
line = self._file.readline()
|
||||||
_file.seek(0, 2) # Move to the end of the file
|
while line:
|
||||||
while True:
|
self.line_count += 1
|
||||||
line = _file.readline()
|
self._process_line(line)
|
||||||
if line:
|
line = self._file.readline()
|
||||||
self.line_count += 1
|
|
||||||
if self.line_count % 100 == 0:
|
|
||||||
self.log.info("Statistics: %s", self.get_statistic_info())
|
|
||||||
else:
|
|
||||||
self.log.debug("Statistics: %s", self.get_statistic_info())
|
|
||||||
|
|
||||||
self._process_line(line)
|
# Check if the trigger has not been triggered for the timeout period
|
||||||
else:
|
if (datetime.now() - self.timestamp_last_active_change) > timedelta(minutes=self.timeout_in_minutes):
|
||||||
await asyncio.sleep(0.1)
|
self.set_inactive()
|
||||||
# Check if the trigger has not been triggered for the timeout period
|
|
||||||
if (datetime.now() - self.timestamp_last_active_change) > timedelta(minutes=self.timeout_in_minutes):
|
|
||||||
self.set_inactive()
|
|
||||||
|
|
||||||
# Detect if file has been rotated
|
except FileNotFoundError:
|
||||||
if os.stat(self.file).st_ino != current_inode:
|
self.log.debug("file not found: %s", self.file)
|
||||||
self.log.info("File %s rolled over. Reopening...", self.file)
|
|
||||||
break # Exit inner loop to reopen file
|
|
||||||
except FileNotFoundError:
|
|
||||||
self.log.debug("file not found: %s", self.file)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
async def async_run(self) -> None:
|
|
||||||
await self.read_log()
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class RoonTrigger(Trigger):
|
|||||||
def __init__(self, _config: Config) -> None:
|
def __init__(self, _config: Config) -> None:
|
||||||
super().__init__(_config)
|
super().__init__(_config)
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
self.roonapi.register_state_callback(self.roon_state_callback, "zones_changed")
|
||||||
|
|
||||||
def set_status(self) -> None:
|
def set_status(self) -> None:
|
||||||
zones_state = list()
|
zones_state = list()
|
||||||
@@ -99,5 +100,6 @@ class RoonTrigger(Trigger):
|
|||||||
)
|
)
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
async def async_run(self) -> None:
|
def trigger_code(self) -> None:
|
||||||
self.roonapi.register_state_callback(self.roon_state_callback, "zones_changed")
|
# this check works with the callback function of roonapi
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
from cpu_governor_auto_adjust.trigger import Trigger
|
from cpu_governor_auto_adjust.trigger import Trigger
|
||||||
from cpu_governor_auto_adjust.config import Config
|
from cpu_governor_auto_adjust.config import Config
|
||||||
import random
|
import random
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class TestTrigger1(Trigger):
|
class TestTrigger1(Trigger):
|
||||||
def __init__(self, _config: Config) -> None:
|
def __init__(self, _config: Config) -> None:
|
||||||
super().__init__(_config)
|
super().__init__(_config)
|
||||||
|
|
||||||
async def async_run(self) -> None:
|
def trigger_code(self) -> None:
|
||||||
while True:
|
choices = [False, True]
|
||||||
choices = [False, True]
|
self.log.debug("run check code of %s", self.__class__.__name__)
|
||||||
self.log.debug("run check code of %s", self.__class__.__name__)
|
self.active = random.choice(choices)
|
||||||
self.active = random.choice(choices)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from cpu_governor_auto_adjust.trigger import Trigger
|
from cpu_governor_auto_adjust.trigger import Trigger
|
||||||
from cpu_governor_auto_adjust.config import Config
|
from cpu_governor_auto_adjust.config import Config
|
||||||
from datetime import datetime, time
|
from datetime import datetime, time
|
||||||
import asyncio
|
|
||||||
|
|
||||||
|
|
||||||
class TimeTrigger(Trigger):
|
class TimeTrigger(Trigger):
|
||||||
@@ -15,16 +14,12 @@ class TimeTrigger(Trigger):
|
|||||||
def end_time(self) -> time:
|
def end_time(self) -> time:
|
||||||
return datetime.strptime(self.config.custom_config['endTime'], '%H:%M').time()
|
return datetime.strptime(self.config.custom_config['endTime'], '%H:%M').time()
|
||||||
|
|
||||||
async def check_time(self) -> None:
|
def trigger_code(self) -> None:
|
||||||
while True:
|
active_current = self.active
|
||||||
active_current = self.active
|
active_new = self.start_time() <= datetime.now().time() <= self.end_time()
|
||||||
active_new = self.start_time() <= datetime.now().time() <= self.end_time()
|
if not active_current and active_new:
|
||||||
if not active_current and active_new:
|
self.log.info("activating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
||||||
self.log.info("activating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
elif active_current and not active_new:
|
||||||
elif active_current and not active_new:
|
self.log.info("deactivating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
||||||
self.log.info("deactivating trigger, start time: %s, end time: %s, governor: %s", self.start_time(), self.end_time(), self.governor.name)
|
self.active = active_new
|
||||||
self.active = active_new
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
async def async_run(self) -> None:
|
|
||||||
await self.check_time()
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "cpu_governor_auto_adjust"
|
name = "cpu_governor_auto_adjust"
|
||||||
version = "0.4.0"
|
version = "0.5.6"
|
||||||
description = "This application has been developed to automatically change cpu governor based on certain triggers."
|
description = "This application has been developed to automatically change cpu governor based on certain triggers."
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Martin Reurekas", email = "martin@semrks.nl" }
|
{ name = "Martin Reurekas", email = "martin@semrks.nl" }
|
||||||
|
|||||||
Reference in New Issue
Block a user