From f3fcc5604ce8e049781e66cc1b6f7ba51efc27c6 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Tue, 27 Dec 2022 17:26:11 +0100 Subject: [PATCH] add sensors and rules --- app/Command/AbstractCommand.php | 13 ++ app/Command/Config.php | 191 +++++++++++++++++++++------- app/Command/ShowConfig.php | 22 ++++ app/FanConfig.php | 161 ++++++++++++++++++++--- app/Kernel.php | 5 +- app/Model.php | 16 +++ app/Model/Concerns/DetectsHwmon.php | 24 ++++ app/Model/Fan.php | 14 ++ app/Model/Fan/HwmonFan.php | 130 +++++++++++++++++++ app/Model/Pwm.php | 10 -- app/Model/Rule.php | 18 +++ app/Model/Rule/CurveRule.php | 107 ++++++++++++++++ app/Model/Sensor.php | 10 ++ app/Model/Sensor/CommandSensor.php | 59 +++++++++ app/Model/Sensor/HwmonSensor.php | 58 +++++++++ composer.json | 3 +- composer.lock | 53 +++++++- 17 files changed, 813 insertions(+), 81 deletions(-) create mode 100644 app/Command/ShowConfig.php create mode 100644 app/Model.php create mode 100644 app/Model/Concerns/DetectsHwmon.php create mode 100644 app/Model/Fan.php create mode 100644 app/Model/Fan/HwmonFan.php delete mode 100644 app/Model/Pwm.php create mode 100644 app/Model/Rule.php create mode 100644 app/Model/Rule/CurveRule.php create mode 100644 app/Model/Sensor.php create mode 100644 app/Model/Sensor/CommandSensor.php create mode 100644 app/Model/Sensor/HwmonSensor.php diff --git a/app/Command/AbstractCommand.php b/app/Command/AbstractCommand.php index b24290b..8593577 100644 --- a/app/Command/AbstractCommand.php +++ b/app/Command/AbstractCommand.php @@ -8,6 +8,7 @@ use GetOpt\Command; use GetOpt\GetOpt; use GetOpt\Option; use Hugga\Console; +use Hugga\QuestionInterface; abstract class AbstractCommand extends Command { @@ -33,4 +34,16 @@ abstract class AbstractCommand extends Command } abstract public function handle(GetOpt $getOpt): int; + + /** + * Ask a simple question or the given question. + * + * @param QuestionInterface|string $question + * @param mixed $default + * @return mixed + */ + protected function ask($question, $default = null) + { + return $this->console->ask($question, $default); + } } diff --git a/app/Command/Config.php b/app/Command/Config.php index 2756c21..6b251f2 100644 --- a/app/Command/Config.php +++ b/app/Command/Config.php @@ -3,6 +3,7 @@ namespace App\Command; use App\Application; +use App\Model\Fan\HwmonFan; use GetOpt\GetOpt; use Hugga\Console; use Hugga\Input\Question\Choice; @@ -32,6 +33,11 @@ class Config extends AbstractCommand } $fanConfig = $this->app->fanConfig; + try { + $fanConfig->readConfig(); + } catch (\Throwable $e) { + $this->error('Unable to read config: ' . $e->getMessage()); + } if (!$fanConfig->hasFans()) { if (!$this->initializeConfig()) { return 1; @@ -48,15 +54,20 @@ class Config extends AbstractCommand 'exit without saving', ], 'What next?', 'save and exit'); do { - $answer = $this->console->ask($actions); + $fanConfig->display($this->console); + $answer = $this->ask($actions); switch ($answer) { + case 'edit a fan': + $fan = $this->selectFan($fanConfig->getFans()); + break; case 'exit without saving': break 2; case 'save and exit': -// $fanConfig->save(); + $fanConfig->save(); break 2; default: - $this->info('not implemented'); + $this->warn('not implemented'); + break; } } while (true); @@ -79,14 +90,56 @@ class Config extends AbstractCommand $this->info('No fans configured. Detecting fans...'); $fans = $this->detectControllableFans(); if (count($fans) > 0) { - // for each fan - // ask if it should be added - // ask for a name - // test min speed - // ask for a pwm limit - // add the fan to the config - - $fanConfig->setFans($fans); + foreach ($fans as $fan) { + $this->line(''); + $this->info('Fan ' . $fan->name); + $start = $this->detectStartValue($fan); + $fan->setStartValue($start); + do { + $answer = $this->ask(new Choice([ + 'name fan', + 'adjust start value', + 'define limit', + 'add fan', + 'skip fan', + ], 'What next?', 'name fan')); + switch ($answer) { + case 'adjust start value': + do { + $fan->setPwm(0); + $startValue = $this->ask('What value to try?', $start); + $fan->setPwm($startValue); + } while (!$this->ask(new Confirmation('Use that value?'))); + $fan->resetState(); + $fan->setStartValue($startValue); + break; + case 'define limit': + do { + $fan->setPwm(255); + $maxValue = $this->ask('What value to try?', 255); + $fan->setPwm($maxValue); + $this->waitForStableRpm($fan); + $this->line('fan runs at ' . $fan->getCurrentSpeed() . ' rpm now'); + } while (!$this->ask(new Confirmation('Use that value?'))); + $fan->resetState(); + $fan->setMaxValue($maxValue); + break; + case 'name fan': + $name = $this->ask('Name of the fan: ', $fan->name); + $fan->name = $name; + break; + case 'add fan': + $fanConfig->addFan($fan); + break 2; + case 'skip fan': + break 2; + default: + $this->warn('not implemented'); + break; + } + } while (true); + } + $fanConfig->save(); return true; } @@ -94,6 +147,9 @@ class Config extends AbstractCommand return false; } + /** + * @return array|HwmonFan[] + */ protected function detectControllableFans(): array { $fans = []; @@ -108,37 +164,35 @@ class Config extends AbstractCommand $pwm = preg_replace('~fan(\d+)~', 'pwm$1', $fan); $name = $hwmon . '/' . $fan; + $options = [ + 'hwmon' => $hwmon, + 'fan' => $fan, + 'pwm' => $pwm, + ]; $inputs[$name] = [ - 'input' => $input, - 'pwm' => preg_replace('~fan(\d+)_input~', 'pwm$1', $input), + 'fan' => new HwmonFan($name, $options), + 'options' => $options, 'hwmon' => 'hwmon' . $match[2], - 'name' => $name, - 'type' => 'hwmon', - 'options' => [ - 'hwmon' => $hwmon, - 'fan' => $fan, - 'pwm' => $pwm, - ], ]; return $inputs; }, []); $unknownInputs = []; foreach ($inputs as $name => $input) { - [$min, $max] = $this->testPwm($input['pwm'], $input['input']); + [$min, $max] = $this->testPwm($input['fan']); if ($max === 0 || $min > $max*0.4) { $unknownInputs[$name] = $input; continue; } - $this->info(sprintf('%s at full speed: %d; stopped: %d', $name, $max, $min)); - $fans[] = $input; + $this->line(sprintf('%s at full speed: %d; stopped: %d', $name, $max, $min)); + $fans[] = $input['fan']; } if (count($unknownInputs) > 0) { - $this->info('Some fans could not be assigned to a pwm. Trying remaining pwms...'); - $assignedPwms = array_map(fn($fan) => $fan['pwm'], $fans); + $this->info(PHP_EOL . 'Some fans could not be assigned to a pwm. Trying remaining pwms...'); + $assignedPwms = array_map(fn(HwmonFan $fan) => $fan->toArray()['options']['pwm'], $fans); $pwms = array_reduce(glob(HWMON_PATH . '/hwmon*/pwm*'), function ($pwms, $pwm) use ($assignedPwms) { - if (!preg_match('~(hwmon(\d+)/pwm(\d+))$~', $pwm, $match) || in_array($pwm, $assignedPwms)) { + if (!preg_match('~(hwmon(\d+)/(pwm\d+))$~', $pwm, $match) || in_array(basename($pwm), $assignedPwms)) { return $pwms; } @@ -146,7 +200,7 @@ class Config extends AbstractCommand if (!isset($pwms[$hwmon])) { $pwms[$hwmon] = []; } - $pwms[$hwmon][] = $pwm; + $pwms[$hwmon][] = basename($pwm); return $pwms; }, []); @@ -155,49 +209,90 @@ class Config extends AbstractCommand continue; } foreach ($pwms[$input['hwmon']] as $pwm) { - if ($input['pwm'] !== $pwm) { + $name = $input['fan']->name; + $options = $input['options']; + if ($options['pwm'] === $pwm) { continue; } - [$min, $max] = $this->testPwm($pwm, $input['input']); + $options['pwm'] = $pwm; + $fan = new HwmonFan($name, $options); + + [$min, $max] = $this->testPwm($fan); if ($max === 0 || $min > $max*0.4) { continue; } - $input['pwm'] = $pwm; - $input['options']['pwm'] = basename($pwm); - $fans[] = $input; + $fans[] = $fan; continue 2; } $this->info($id . ' not connected or not controllable'); } } - return array_map(function ($fan) { - unset($fan['input'], $fan['pwm']); - return $fan; - }, $fans); + return $fans; } - protected function testPwm($pwm, $input): array + protected function testPwm(HwmonFan $fan): array { - $currentEnable = file_get_contents($pwm . '_enable'); - - file_put_contents($pwm . '_enable', '1'); - file_put_contents($pwm, '255'); - sleep(4); - $fullSpeed = (int)file_get_contents($input); + $fan->setPwm(255); + $this->waitForStableRpm($fan); + $fullSpeed = $fan->getCurrentSpeed(); if ($fullSpeed === 0) { - file_put_contents($pwm . '_enable', $currentEnable); + $fan->resetState(); return [0, 0]; } - file_put_contents($pwm, '0'); - sleep(10); - $stopSpeed = (int)file_get_contents($input); + $fan->setPwm(0); + $this->waitForStableRpm($fan); + $stopSpeed = $fan->getCurrentSpeed(); - file_put_contents($pwm . '_enable', $currentEnable); + $fan->resetState(); return [$stopSpeed, $fullSpeed]; } + + protected function detectStartValue(HwmonFan $fan) + { + $this->info('Detecting start value for ' . $fan->name); + $fan->setPwm(0); + $this->waitForStableRpm($fan); + + $minSpeed = $fan->getCurrentSpeed() * 1.1; + $pwm = 0; + do { + $fan->setPwm($pwm += 5); + $this->waitForStableRpm($fan); + $currentSpeed = $fan->getCurrentSpeed(); + $this->line(sprintf('%d rpm with pwm value %d', $currentSpeed, $pwm)); + } while ($minSpeed >= $currentSpeed); + + $fan->resetState(); + return $pwm; + } + + protected function waitForStableRpm(HwmonFan $fan) + { + static $waitTime = 600_000; // in micro seconds + static $count = 4; + $readings = []; + for ($i = 0; $i < $count; $i++) { + $readings[] = $fan->getCurrentSpeed(); + usleep($waitTime); + } + while (array_sum($readings) > 0 && // fan has stopped + (max($readings) - min($readings)) / (array_sum($readings) / $count) * 100 > 2 + ) { + $this->line(sprintf( + 'min: %d rpm, max: %d rpm, avg: %d rpm, variance: %f%%', + min($readings), + max($readings), + array_sum($readings) / $count, + (max($readings) - min($readings)) / (array_sum($readings) / $count) * 100 + ), Console::WEIGHT_DEBUG); + array_shift($readings); + $readings[] = $fan->getCurrentSpeed(); + usleep($waitTime); + } + } } diff --git a/app/Command/ShowConfig.php b/app/Command/ShowConfig.php new file mode 100644 index 0000000..d002120 --- /dev/null +++ b/app/Command/ShowConfig.php @@ -0,0 +1,22 @@ +<?php + +namespace App\Command; + +use GetOpt\GetOpt; + +class ShowConfig extends AbstractCommand +{ + protected $name = 'config:show'; + + + public function handle(GetOpt $getOpt): int + { + $fanConfig = $this->app->fanConfig; + if (!$fanConfig->hasFans()) { + $this->error('No fans configured'); + return 1; + } + $fanConfig->display($this->console); + return 0; + } +} diff --git a/app/FanConfig.php b/app/FanConfig.php index 910caa8..e9760eb 100644 --- a/app/FanConfig.php +++ b/app/FanConfig.php @@ -2,56 +2,117 @@ namespace App; +use App\Model\Fan; +use App\Model\Rule; +use App\Model\Sensor; +use Hugga\Console; +use Hugga\Output\Drawing\Table; + class FanConfig { protected $path = '/dev/null'; + + protected $loaded = false; + + /** @var array|Fan[] */ protected $fans = []; + /** @var array|Sensor[] */ + protected $sensors = []; + + /** @var array|Rule[] */ + protected $rules = []; + + public function __construct(string $path) + { + $this->path = $path; + } + public function hasFans(): bool { + $this->load(); return count($this->fans) > 0; } - public function setFans(array $fans): self + public function addFan(Fan $fan): self { - // do we validate that? or was it validated already? - $this->fans = $fans; + $this->load(); + $this->fans[$fan->name] = $fan; return $this; } - public static function readConfig(string $path): self + /** @return Fan[]|array */ + public function getFans(): array + { + $this->load(); + return $this->fans; + } + + protected function load() { - $self = new FanConfig(); - $self->path = $path; - if (!file_exists($path)) { - return $self; + if (!$this->loaded) { + $this->readConfig(); } + } - $cfg = include($path); + public function readConfig(): self + { + if (!file_exists($this->path)) { + return $this; + } + + $this->loaded = true; + $cfg = include($this->path); // validate config if (!is_array($cfg)) { throw new \InvalidArgumentException('given configuration is not a fan-ctrl config'); } - $self = new FanConfig(); - $self->path = $path; - $self->fans = $cfg['fans'] ?? []; - return $self; + foreach ($cfg['fans'] ?? [] as $fanCfg) { + $class = $fanCfg['type']; + $fan = new $class($fanCfg['name'], $fanCfg['options']); + $this->fans[$fan->name] = $fan; + } + + foreach ($cfg['sensors'] ?? [] as $sensorCfg) { + $class = $sensorCfg['type']; + $sensor = new $class($sensorCfg['name'], $sensorCfg['options']); + $this->sensors[$sensor->name] = $sensor; + } + + foreach ($cfg['rules'] ?? [] as $ruleCfg) { + $class = $ruleCfg['type']; + $fans = array_map(fn($name) => $this->fans[$name] ?? null, $ruleCfg['fans']); + if (in_array(null, $fans, true)) { + continue; + } + $sensor = $this->sensors[$ruleCfg['sensor']] ?? null; + if (!$sensor) { + continue; + } + $rule = new $class($fans, $sensor, $ruleCfg['options']); + $this->rules[] = $rule; + } + + return $this; } public function save() { -// ob_start(); -// $fans = $this->fans; -// include __DIR__ . '/../resources/config-template.php'; -// $code = ob_get_clean(); $code = '<?php' . PHP_EOL . PHP_EOL . 'return ' . $this->exportArray([ - 'fans' => $this->fans, + 'fans' => array_map(function (Fan $fan) { + return $fan->toArray(); + }, array_values($this->fans)), + 'sensors' => array_map(function (Sensor $sensor) { + return $sensor->toArray(); + }, array_values($this->sensors)), + 'rules' => array_map(function (Rule $rule) { + return $rule->toArray(); + }, $this->rules), ]) . ';' . PHP_EOL; file_put_contents($this->path, $code); -// exec(__DIR__ . '/../vendor/bin/php-cs-fixer fix ' . $this->path . ' --rules'); } protected function exportArray(array $array, $indentation = 0) @@ -71,4 +132,66 @@ class FanConfig $var .= str_repeat(' ', $indentation) . ']'; return $var; } + + public function display(Console $console) + { + $this->load(); + + $console->info('Fans'); + $table = new Table($console, array_map(function (Fan $fan) { + if ($fan instanceof Fan\HwmonFan) { + $options = $fan->toArray()['options']; + return [ + $fan->name, + $options['hwmon'], + $options['fan'], + $options['pwm'], + $options['start'], + $options['max'], + ]; + } + + return [ + $fan->name, + null, + null, + null, + null, + ]; + }, $this->fans), ['Name', 'hwmon', 'Fan', 'PWM', 'start', 'max']); + $table->column(4, ['width' => 5]); + $table->draw(); + + $console->line(''); + + $console->info('Sensors'); + $table = new Table($console, array_map(function (Sensor $sensor) { + $options = $sensor->toArray()['options']; + $result = [$sensor->name]; + if ($sensor instanceof Sensor\HwmonSensor) { + $result[] = $options['hwmon']; + $result[] = $options['temp']; + } elseif ($sensor instanceof Sensor\CommandSensor) { + $result[] = null; + $result[] = $options['command']; + } else { + $result[] = null; + $result[] = null; + } + return $result; + }, $this->sensors), ['Name', 'hwmon', 'sensor / command']); + $table->draw(); + + $console->line(''); + + $console->info('Rules'); + $table = new Table($console, array_map(function (Rule $rule) { + return [ + implode(', ', array_map(fn($fan) => $fan->name, $rule->fans)), + $rule->sensor->name, + $rule->describe(), + ]; + }, $this->rules), ['Fans', 'Sensor', 'Description']); + $table->draw(); + } } diff --git a/app/Kernel.php b/app/Kernel.php index cf1d790..dfbbd04 100644 --- a/app/Kernel.php +++ b/app/Kernel.php @@ -15,7 +15,8 @@ class Kernel extends \Riki\Kernel { /** @var string[] */ protected static $commands = [ - \App\Command\Config::class, + Command\Config::class, + Command\ShowConfig::class, ]; /** @var \Riki\Application|Application */ @@ -65,7 +66,7 @@ class Kernel extends \Riki\Kernel } $configPath = $getOpt->getOption('config') ?: $this->app->config->defaultConfigPath; - $this->app->instance('fanConfig', FanConfig::readConfig($configPath)); + $this->app->instance('fanConfig', new FanConfig($configPath)); $command = $getOpt->getCommand(); if (!$command || $getOpt->getOption('help')) { diff --git a/app/Model.php b/app/Model.php new file mode 100644 index 0000000..ee87cc2 --- /dev/null +++ b/app/Model.php @@ -0,0 +1,16 @@ +<?php + +namespace App; + +abstract class Model +{ + /** @var string */ + public $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + abstract public function toArray(): array; +} diff --git a/app/Model/Concerns/DetectsHwmon.php b/app/Model/Concerns/DetectsHwmon.php new file mode 100644 index 0000000..840f3bc --- /dev/null +++ b/app/Model/Concerns/DetectsHwmon.php @@ -0,0 +1,24 @@ +<?php + +namespace App\Model\Concerns; + +trait DetectsHwmon +{ + /** + * Detect the base path of the hwmon with $name + * + * @param string $name + * @return ?string + */ + protected function detectBasePath(string $name): ?string + { + foreach (glob(HWMON_PATH . '/hwmon*/name') as $nameFile) { + $current = trim(file_get_contents($nameFile)); + if ($current === $name) { + return substr($nameFile, 0, -4); + } + } + + return null; + } +} diff --git a/app/Model/Fan.php b/app/Model/Fan.php new file mode 100644 index 0000000..557ea92 --- /dev/null +++ b/app/Model/Fan.php @@ -0,0 +1,14 @@ +<?php + +namespace App\Model; + +use App\Model; + +abstract class Fan extends Model +{ + abstract public function getCurrentSpeed(): int; + + abstract public function setSpeed($percentage); + + abstract public function stopFan(); +} diff --git a/app/Model/Fan/HwmonFan.php b/app/Model/Fan/HwmonFan.php new file mode 100644 index 0000000..63a1dd5 --- /dev/null +++ b/app/Model/Fan/HwmonFan.php @@ -0,0 +1,130 @@ +<?php + +namespace App\Model\Fan; + +use App\Model\Concerns\DetectsHwmon; +use App\Model\Fan; + +class HwmonFan extends Fan +{ + use DetectsHwmon; + + const ENABLE_FULL_SPEED = 0; + const ENABLE_MANUAL = 1; + const ENABLE_MIN_AUTOMATIC = 2; + + /** @var string */ + protected $hwmon; + + /** @var string */ + protected $basePath; + + /** @var string */ + protected $fan; + + /** @var string */ + protected $pwm; + + /** @var int */ + protected $startValue; + + /** @var int */ + protected $maxValue; + + /** @var array */ + protected $currentState = []; + + public function __construct(string $name, array $options) + { + parent::__construct($name); + if (!isset($options['hwmon']) || !isset($options['fan']) || !isset($options['pwm'])) { + throw new \InvalidArgumentException('The options hwmon, fan and pwm are required'); + } + $this->hwmon = $options['hwmon']; + $this->basePath = $this->detectBasePath($this->hwmon); + $this->fan = $options['fan']; + $this->pwm = $options['pwm']; + $this->startValue = $options['start'] ?? 35; // do we need to configure a default start value? + $this->maxValue = $options['max'] ?? 255; + + if (!$this->basePath) { + throw new \InvalidArgumentException('The hardware monitor ' . $this->hwmon . ' is not available'); + } + + if (!file_exists($this->basePath . $this->fan . '_input')) { + throw new \InvalidArgumentException('The fan ' . $this->fan . ' is not available'); + } + if (!file_exists($this->basePath . $this->pwm)) { + throw new \InvalidArgumentException('The pwm ' . $this->pwm . ' is not available'); + } + + $this->updateCurrentState(); + } + + public function getCurrentSpeed(): int + { + return (int)file_get_contents($this->basePath . $this->fan . '_input'); + } + + public function setStartValue(int $start): self + { + $this->startValue = $start; + return $this; + } + + public function setMaxValue(int $max): self + { + $this->maxValue = $max; + return $this; + } + + public function setPwm(int $value) + { + if ($value < 0 or $value > 255) { + throw new \InvalidArgumentException('$value has to be between 0 and 255'); + } + file_put_contents($this->basePath . $this->pwm . '_enable', 1); + file_put_contents($this->basePath . $this->pwm, $value); + } + + public function setSpeed($percentage) + { + $this->setPwm($this->startValue + ($this->maxValue - $this->startValue) * $percentage / 100); + } + + public function stopFan() + { + $this->setPwm(0); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'type' => HwmonFan::class, + 'options' => [ + 'hwmon' => $this->hwmon, + 'fan' => $this->fan, + 'pwm' => $this->pwm, + 'start' => $this->startValue, + 'max' => $this->maxValue, + ], + ]; + } + + protected function updateCurrentState() + { + $this->currentState = [ + '' => file_get_contents($this->basePath . $this->pwm), + '_enable' => file_get_contents($this->basePath . $this->pwm . '_enable'), + '_mode' => file_get_contents($this->basePath . $this->pwm . '_mode'), + ]; + } + + public function resetState() + { + foreach ($this->currentState as $name => $value) { + file_put_contents($this->basePath . $this->pwm . $name, $value); + } + } +} diff --git a/app/Model/Pwm.php b/app/Model/Pwm.php deleted file mode 100644 index d874a37..0000000 --- a/app/Model/Pwm.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php - -namespace App\Model; - -class Pwm -{ - const ENABLE_FULL_SPEED = 0; - const ENABLE_MANUAL = 1; - const ENABLE_MIN_AUTOMATIC = 2; -} diff --git a/app/Model/Rule.php b/app/Model/Rule.php new file mode 100644 index 0000000..622c51f --- /dev/null +++ b/app/Model/Rule.php @@ -0,0 +1,18 @@ +<?php + +namespace App\Model; + +use App\Model; + +abstract class Rule extends Model +{ + /** @var Fan[]|array */ + public $fans; + + /** @var Sensor */ + public $sensor; + + abstract public function apply(); + + abstract public function describe(): string; +} diff --git a/app/Model/Rule/CurveRule.php b/app/Model/Rule/CurveRule.php new file mode 100644 index 0000000..29635cb --- /dev/null +++ b/app/Model/Rule/CurveRule.php @@ -0,0 +1,107 @@ +<?php + +namespace App\Model\Rule; + +use App\Model\Fan; +use App\Model\Rule; +use App\Model\Sensor; + +class CurveRule extends Rule +{ + /** @var array|Fan[] */ + public $fans = []; + + /** @var Sensor */ + public $sensor; + + /** @var bool */ + protected $alwaysOn = true; + + /** @var array */ + protected $points = []; + + public function __construct(array $fans, Sensor $sensor, array $options) + { + $name = implode(',', array_map(fn($fan) => $fan->name, $fans)); + parent::__construct($name); + + if (!isset($options['points']) || !is_array($options['points'])) { + throw new \InvalidArgumentException('The option points is required and has to be an array'); + } + + $this->fans = $fans; + $this->sensor = $sensor; + + $this->alwaysOn = $options['alwaysOn'] ?? true; + $this->points = $options['points']; + ksort($this->points); + $lastLimit = null; + foreach ($this->points as $percentage => $limit) { + if ($lastLimit && $lastLimit > $limit) { + throw new \InvalidArgumentException('Each point has to have a higher temperature limit than the last'); + } + } + } + + public function apply() + { + $temp = $this->sensor->getTemperature(); + $lastPercentage = null; + $lastLimit = null; + foreach ($this->points as $percentage => $limit) { + if ($temp >= $limit) { + $lastPercentage = $percentage; + $lastLimit = $limit; + continue; + // somewhere between $lastV and $v + + } + if (!$this->alwaysOn && $lastPercentage === null) { + // stop the fans if they should not run all the time and first limit not reached + $this->stopFans(); + } elseif ($lastPercentage === null) { + // run the fans with the lowest percentage value before the first point + $this->setSpeed($percentage); + } else { + $factor = $temp - $lastLimit / ($limit - $lastLimit); + $this->setSpeed(($percentage - $lastPercentage) * $factor); + } + } + } + + public function describe(): string + { + $initialSpeed = array_keys($this->points)[0]; + $minLimit = array_values($this->points)[0]; + $description = $this->alwaysOn ? 'With ' . $initialSpeed . '% speed' : 'Off'; + $description .= ' until ' . $minLimit . '° C.'; + return $description; + } + + public function toArray(): array + { + return [ + 'fans' => array_map(fn($fan) => $fan->name, $this->fans), + 'sensor' => $this->sensor->name, + 'type' => CurveRule::class, + 'options' => [ + 'alwaysOn' => $this->alwaysOn, + 'points' => $this->points, + ], + ]; + } + + protected function stopFans() + { + foreach ($this->fans as $fan) { + $fan->stopFan(); + } + } + + protected function setSpeed(float $percentage) + { + foreach ($this->fans as $fan) { + $fan->setSpeed($percentage); + } + } +} diff --git a/app/Model/Sensor.php b/app/Model/Sensor.php new file mode 100644 index 0000000..534b682 --- /dev/null +++ b/app/Model/Sensor.php @@ -0,0 +1,10 @@ +<?php + +namespace App\Model; + +use App\Model; + +abstract class Sensor extends Model +{ + abstract public function getTemperature(): int; +} diff --git a/app/Model/Sensor/CommandSensor.php b/app/Model/Sensor/CommandSensor.php new file mode 100644 index 0000000..c2bb405 --- /dev/null +++ b/app/Model/Sensor/CommandSensor.php @@ -0,0 +1,59 @@ +<?php + +namespace App\Model\Sensor; + +use App\Model\Sensor; +use MathParser\Interpreting\Evaluator; +use MathParser\Parsing\Nodes\Node; +use MathParser\StdMathParser; + +class CommandSensor extends Sensor +{ + /** @var string */ + protected $command; + + /** @var string */ + protected $conversion; + + /** @var Node */ + protected $ast; + + public function __construct(string $name, array $options) + { + parent::__construct($name); + if (!isset($options['command'])) { + throw new \InvalidArgumentException('The option command is required'); + } + + $this->command = $options['command']; + + if (isset($options['conversion'])) { + $parser = new StdMathParser(); + $this->ast = $parser->parse($options['conversion']); + } + $this->conversion = $options['conversion'] ?? null; + } + + + public function getTemperature(): int + { + $result = exec(escapeshellcmd($this->command)); + if ($this->ast) { + $eval = new Evaluator(['x' => (double)$result]); + $result = $this->ast->accept($eval); + } + return (int)round($result); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'type' => CommandSensor::class, + 'options' => [ + 'command' => $this->command, + 'conversion' => $this->conversion, + ] + ]; + } +} diff --git a/app/Model/Sensor/HwmonSensor.php b/app/Model/Sensor/HwmonSensor.php new file mode 100644 index 0000000..3401643 --- /dev/null +++ b/app/Model/Sensor/HwmonSensor.php @@ -0,0 +1,58 @@ +<?php + +namespace App\Model\Sensor; + +use App\Model\Concerns\DetectsHwmon; +use App\Model\Sensor; + +class HwmonSensor extends Sensor +{ + use DetectsHwmon; + + /** @var string */ + protected $hwmon; + + /** @var string */ + protected $basePath; + + /** @var string */ + protected $temp; + + public function __construct(string $name, array $options) + { + parent::__construct($name); + if (!isset($options['hwmon']) || !isset($options['temp'])) { + throw new \InvalidArgumentException('The options hwmon and temp are required'); + } + + $this->hwmon = $options['hwmon']; + $this->basePath = $this->detectBasePath($this->hwmon); + $this->temp = $options['temp']; + + if (!$this->basePath) { + throw new \InvalidArgumentException('The hardware monitor ' . $this->hwmon . ' is not available'); + } + + if (!file_exists($this->basePath . $this->temp . '_input')) { + throw new \InvalidArgumentException('The sensor ' . $this->temp . ' is not available'); + } + } + + public function getTemperature(): int + { + $temp = (int)file_get_contents($this->basePath . $this->temp . '_input'); + return (int)round($temp/1000); + } + + public function toArray(): array + { + return [ + 'name' => $this->name, + 'type' => HwmonSensor::class, + 'options' => [ + 'hwmon' => $this->hwmon, + 'temp' => $this->temp, + ], + ]; + } +} diff --git a/composer.json b/composer.json index 3c1888e..d98f179 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "filp/whoops": "^2.14", "monolog/monolog": "^1.9", "ext-pcntl": "*", - "php": ">=7.4" + "php": ">=7.4", + "mossadal/math-parser": "^1.3" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index b5683fa..ff722eb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ae650b3e47d3fef6f9aba60ffeb5b60", + "content-hash": "cddd1ddd64b18141de6444950d1076f4", "packages": [ { "name": "filp/whoops", @@ -163,6 +163,57 @@ ], "time": "2022-06-09T08:53:42+00:00" }, + { + "name": "mossadal/math-parser", + "version": "v1.3.16", + "source": { + "type": "git", + "url": "https://github.com/mossadal/math-parser.git", + "reference": "981b03ca603fd281049e092d75245ac029e13dec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mossadal/math-parser/zipball/981b03ca603fd281049e092d75245ac029e13dec", + "reference": "981b03ca603fd281049e092d75245ac029e13dec", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpdocumentor/phpdocumentor": "2.*", + "phpunit/php-code-coverage": "6.0.*", + "phpunit/phpunit": "7.3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "MathParser\\": "src/MathParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Frank Wikström", + "email": "frank@mossadal.se", + "role": "Developer" + } + ], + "description": "PHP parser for mathematical expressions, including elementary functions, variables and implicit multiplication. Also supports symbolic differentiation.", + "homepage": "https://github.com/mossadal/math-parser", + "keywords": [ + "mathematics", + "parser" + ], + "support": { + "issues": "https://github.com/mossadal/math-parser/issues", + "source": "https://github.com/mossadal/math-parser/tree/master" + }, + "time": "2018-09-15T22:20:34+00:00" + }, { "name": "nicmart/string-template", "version": "v0.1.3", -- GitLab