#!/usr/bin/env python3 """Calibrazione Ambilight: fascia bianca e regolazioni live via ambilight-ctl.""" import json import os import sys from pathlib import Path from PySide6.QtCore import QProcess, QTimer, Qt from PySide6.QtWidgets import (QApplication, QFileDialog, QHBoxLayout, QLabel, QMessageBox, QPushButton, QSlider, QVBoxLayout, QWidget) CTL = str(Path.home() / ".local/bin/ambilight-ctl") FIELDS = (("gain_r", "Rosso", 0, 200), ("gain_g", "Verde", 0, 200), ("gain_b", "Blu", 0, 200), ("brightness", "Luminosità", 5, 200)) class Calibration(QWidget): def __init__(self): super().__init__() self.setWindowTitle("Calibrazione Ambilight") self.values = {} self.pending = {} self.initialized = False self.process = QProcess(self) self.process.finished.connect(self.finished) self.process.errorOccurred.connect(self.process_error) self.current = None self.debounce = QTimer(self, interval=80, singleShot=True) self.debounce.timeout.connect(self.pump) outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) outer.setSpacing(0) self.white = QWidget() self.white.setStyleSheet("background: white;") outer.addWidget(self.white, 30) # 30%, maggiore della banda predefinita del 12% panel = QWidget() panel.setStyleSheet("background: #20242b; color: #eeeeee;") outer.addWidget(panel, 70) layout = QVBoxLayout(panel) layout.setContentsMargins(50, 20, 50, 24) title = QLabel("Calibrazione Ambilight") title.setStyleSheet("font-size: 24px; font-weight: bold;") layout.addWidget(title) hint = QLabel("La fascia superiore è bianca. Le modifiche si applicano e salvano nel demone in tempo reale.") hint.setWordWrap(True) layout.addWidget(hint) self.status = QLabel("Connessione al demone…") layout.addWidget(self.status) self.sliders = {} self.labels = {} for key, name, lo, hi in FIELDS: row = QHBoxLayout() label = QLabel(name) label.setMinimumWidth(120) row.addWidget(label) slider = QSlider(Qt.Horizontal) slider.setRange(lo, hi) slider.setEnabled(False) row.addWidget(slider, 1) value_label = QLabel("—") value_label.setMinimumWidth(55) row.addWidget(value_label) slider.valueChanged.connect(lambda value, k=key: self.changed(k, value)) self.sliders[key], self.labels[key] = slider, value_label layout.addLayout(row) layout.addStretch(1) buttons = QHBoxLayout() save = QPushButton("Esporta profilo JSON…") save.clicked.connect(self.export_profile) buttons.addWidget(save) buttons.addStretch(1) close = QPushButton("Chiudi") close.clicked.connect(self.close) buttons.addWidget(close) layout.addLayout(buttons) self.call("state", []) def call(self, kind, args): self.current = kind self.process.start(CTL, args or ["state"]) def changed(self, key, value): self.labels[key].setText(f"{value}%") if not self.initialized: return self.pending[key] = value / 100.0 self.status.setText("Applicazione in corso…") self.debounce.start() def pump(self): if self.process.state() != QProcess.NotRunning or not self.pending: return key = next(iter(self.pending)) value = self.pending.pop(key) self.call(key, ["set", key, f"{value:.2f}"]) def process_error(self, error): self.status.setText("Errore: impossibile avviare ambilight-ctl; controlla il servizio.") self.current = None def finished(self, code, exit_status): kind, self.current = self.current, None raw = bytes(self.process.readAllStandardOutput()).decode("utf-8", "replace") try: data = json.loads(raw) if code != 0 or data.get("ok") is False: raise ValueError(data.get("error", "servizio non raggiungibile")) state = data.get("state", data) if not state.get("daemon"): raise ValueError(state.get("error", "servizio non raggiungibile")) if kind == "state": for key, _, _, _ in FIELDS: value = round(float(state[key]) * 100) self.sliders[key].setValue(value) self.sliders[key].setEnabled(True) self.values[key] = value / 100 self.initialized = True else: self.values[kind] = float(state[kind]) self.status.setText("Valori applicati e salvati dal demone." if not self.pending else "Applicazione in corso…") except (ValueError, KeyError, TypeError) as exc: self.status.setText(f"Errore: {exc}. Valore non confermato.") self.pump() def export_profile(self): if not self.initialized or self.pending or self.process.state() != QProcess.NotRunning: QMessageBox.warning(self, "Profilo", "Attendi la conferma di tutte le regolazioni.") return filename, _ = QFileDialog.getSaveFileName(self, "Esporta profilo", "ambilight-calibrazione.json", "JSON (*.json)") if not filename: return try: with open(filename, "w", encoding="utf-8") as f: json.dump({key: self.values[key] for key, _, _, _ in FIELDS}, f, indent=2) f.write("\n") self.status.setText(f"Profilo esportato: {filename}") except OSError as exc: QMessageBox.critical(self, "Errore esportazione", str(exc)) def closeEvent(self, event): if self.pending or self.process.state() != QProcess.NotRunning: self.status.setText("Attendi la conferma delle modifiche prima di chiudere.") event.ignore() return event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = Calibration() window.showFullScreen() sys.exit(app.exec())