Skip to content
Snippets Groups Projects
Config.php 9.81 KiB
Newer Older
Thomas Flori's avatar
Thomas Flori committed
<?php

namespace App\Command;

use App\Application;
Thomas Flori's avatar
Thomas Flori committed
use App\Model\Fan\HwmonFan;
Thomas Flori's avatar
Thomas Flori committed
use GetOpt\GetOpt;
use Hugga\Console;
use Hugga\Input\Question\Choice;
use Hugga\Input\Question\Confirmation;

class Config extends AbstractCommand
{
    protected $name = 'config';

    protected $description = 'Generate or update fan configuration';

    public function __construct(Application $app, Console $console)
    {
        parent::__construct($app, $console);
    }

    public function handle(GetOpt $getOpt): int
    {
        if (posix_getuid() !== 0) {
            $this->warn('This programm has to run as root');
            return 1;
        }

        if ($this->getOption('quiet')) {
            $this->warn('This command requires user interaction');
            return 1;
        }

        $fanConfig = $this->app->fanConfig;
Thomas Flori's avatar
Thomas Flori committed
        try {
            $fanConfig->readConfig();
        } catch (\Throwable $e) {
            $this->error('Unable to read config: ' . $e->getMessage());
        }
Thomas Flori's avatar
Thomas Flori committed
        if (!$fanConfig->hasFans()) {
            if (!$this->initializeConfig()) {
                return 1;
            }
        }

        $actions = new Choice([
            'edit a fan',
            'add a sensor',
            'edit a sensor',
            'add a rule',
            'edit a rule',
            'save and exit',
            'exit without saving',
        ], 'What next?', 'save and exit');
        do {
Thomas Flori's avatar
Thomas Flori committed
            $fanConfig->display($this->console);
            $answer = $this->ask($actions);
Thomas Flori's avatar
Thomas Flori committed
            switch ($answer) {
Thomas Flori's avatar
Thomas Flori committed
                case 'edit a fan':
                    $fan = $this->selectFan($fanConfig->getFans());
                    break;
Thomas Flori's avatar
Thomas Flori committed
                case 'exit without saving':
                    break 2;
                case 'save and exit':
Thomas Flori's avatar
Thomas Flori committed
                    $fanConfig->save();
Thomas Flori's avatar
Thomas Flori committed
                    break 2;
                default:
Thomas Flori's avatar
Thomas Flori committed
                    $this->warn('not implemented');
                    break;
Thomas Flori's avatar
Thomas Flori committed
            }
        } while (true);


        // - edit a fan
        // - add a sensor
        // - edit a sensor
        // - add a rule
        // - edit a rule
        // - save and leave
        // - leave without saving

        return 0;
    }

    protected function initializeConfig()
    {
        $fanConfig = $this->app->fanConfig;

        $this->info('No fans configured. Detecting fans...');
        $fans = $this->detectControllableFans();
        if (count($fans) > 0) {
Thomas Flori's avatar
Thomas Flori committed
            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);
            }

Thomas Flori's avatar
Thomas Flori committed
            $fanConfig->save();
            return true;
        }
        $this->error('We could not find any fan to control.');
        return false;
    }

Thomas Flori's avatar
Thomas Flori committed
    /**
     * @return array|HwmonFan[]
     */
Thomas Flori's avatar
Thomas Flori committed
    protected function detectControllableFans(): array
    {
        $fans = [];
        $inputs = glob(HWMON_PATH . '/hwmon*/fan*_input');
        $inputs = array_reduce($inputs, function ($inputs, $input) {
            if (!preg_match('~(hwmon(\d+)/(fan\d+))_input$~', $input, $match)) {
                return $inputs;
            }

            $hwmon = trim(file_get_contents(HWMON_PATH . '/hwmon' . $match[2] . '/name'));
            $fan = $match[3];
            $pwm = preg_replace('~fan(\d+)~', 'pwm$1', $fan);
            $name = $hwmon . '/' . $fan;

Thomas Flori's avatar
Thomas Flori committed
            $options = [
                'hwmon' => $hwmon,
                'fan' => $fan,
                'pwm' => $pwm,
            ];
Thomas Flori's avatar
Thomas Flori committed
            $inputs[$name] = [
Thomas Flori's avatar
Thomas Flori committed
                'fan' => new HwmonFan($name, $options),
                'options' => $options,
Thomas Flori's avatar
Thomas Flori committed
                'hwmon' => 'hwmon' . $match[2],
            ];
            return $inputs;
        }, []);

        $unknownInputs = [];
        foreach ($inputs as $name => $input) {
Thomas Flori's avatar
Thomas Flori committed
            [$min, $max] = $this->testPwm($input['fan']);
Thomas Flori's avatar
Thomas Flori committed
            if ($max === 0 || $min > $max*0.4) {
                $unknownInputs[$name] = $input;
                continue;
            }
Thomas Flori's avatar
Thomas Flori committed
            $this->line(sprintf('%s at full speed: %d; stopped: %d', $name, $max, $min));
            $fans[] = $input['fan'];
Thomas Flori's avatar
Thomas Flori committed
        }

        if (count($unknownInputs) > 0) {
Thomas Flori's avatar
Thomas Flori committed
            $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);
Thomas Flori's avatar
Thomas Flori committed
            $pwms = array_reduce(glob(HWMON_PATH . '/hwmon*/pwm*'), function ($pwms, $pwm) use ($assignedPwms) {
Thomas Flori's avatar
Thomas Flori committed
                if (!preg_match('~(hwmon(\d+)/(pwm\d+))$~', $pwm, $match) || in_array(basename($pwm), $assignedPwms)) {
Thomas Flori's avatar
Thomas Flori committed
                    return $pwms;
                }

                $hwmon = $match[2];
                if (!isset($pwms[$hwmon])) {
                    $pwms[$hwmon] = [];
                }
Thomas Flori's avatar
Thomas Flori committed
                $pwms[$hwmon][] = basename($pwm);
Thomas Flori's avatar
Thomas Flori committed

                return $pwms;
            }, []);
            foreach ($unknownInputs as $id => $input) {
                if (!isset($pwms[$input['hwmon']])) {
                    continue;
                }
                foreach ($pwms[$input['hwmon']] as $pwm) {
Thomas Flori's avatar
Thomas Flori committed
                    $name = $input['fan']->name;
                    $options = $input['options'];
                    if ($options['pwm'] === $pwm) {
Thomas Flori's avatar
Thomas Flori committed
                        continue;
                    }

Thomas Flori's avatar
Thomas Flori committed
                    $options['pwm'] = $pwm;
                    $fan = new HwmonFan($name, $options);

                    [$min, $max] = $this->testPwm($fan);
Thomas Flori's avatar
Thomas Flori committed
                    if ($max === 0 || $min > $max*0.4) {
                        continue;
                    }

Thomas Flori's avatar
Thomas Flori committed
                    $fans[] = $fan;
Thomas Flori's avatar
Thomas Flori committed
                    continue 2;
                }
                $this->info($id . ' not connected or not controllable');
            }
        }

Thomas Flori's avatar
Thomas Flori committed
        return $fans;
Thomas Flori's avatar
Thomas Flori committed
    }

Thomas Flori's avatar
Thomas Flori committed
    protected function testPwm(HwmonFan $fan): array
Thomas Flori's avatar
Thomas Flori committed
    {
Thomas Flori's avatar
Thomas Flori committed
        $fan->setPwm(255);
        $this->waitForStableRpm($fan);
        $fullSpeed = $fan->getCurrentSpeed();
Thomas Flori's avatar
Thomas Flori committed

        if ($fullSpeed === 0) {
Thomas Flori's avatar
Thomas Flori committed
            $fan->resetState();
Thomas Flori's avatar
Thomas Flori committed
            return [0, 0];
        }

Thomas Flori's avatar
Thomas Flori committed
        $fan->setPwm(0);
        $this->waitForStableRpm($fan);
        $stopSpeed = $fan->getCurrentSpeed();
Thomas Flori's avatar
Thomas Flori committed

Thomas Flori's avatar
Thomas Flori committed
        $fan->resetState();
Thomas Flori's avatar
Thomas Flori committed
        return [$stopSpeed, $fullSpeed];
    }
Thomas Flori's avatar
Thomas Flori committed

    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);
        }
    }
Thomas Flori's avatar
Thomas Flori committed
}