Initial implementation: CLI + KDE Plasma 6 widget for Elgato Key Light
- Python CLI (click): discover, status, on/off/toggle, brightness, temperature, info - mDNS discovery via zeroconf for automatic light detection - Shared config at ~/.config/elgato-cli/config.json - KDE Plasma 6 plasmoid with system tray icon, power/brightness/temperature controls - Plasmoid uses XMLHttpRequest directly to light API (no CLI dependency) - Unit tests for API client with mocked HTTP responses
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
|
||||
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
|
||||
<kcfgfile name=""/>
|
||||
<group name="General">
|
||||
<entry name="lightHost" type="String">
|
||||
<label>Elgato light IP address or hostname</label>
|
||||
<default></default>
|
||||
</entry>
|
||||
<entry name="lightPort" type="Int">
|
||||
<label>Elgato light port</label>
|
||||
<default>9123</default>
|
||||
</entry>
|
||||
<entry name="pollInterval" type="Int">
|
||||
<label>Polling interval in milliseconds</label>
|
||||
<default>3000</default>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
@@ -0,0 +1,37 @@
|
||||
import QtQuick
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
MouseArea {
|
||||
id: compactRoot
|
||||
|
||||
property bool wasExpanded: false
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
if (root.lightHost === "" || !root.lightReachable)
|
||||
return "network-disconnect"
|
||||
return root.lightOn ? "brightness-high" : "brightness-low"
|
||||
}
|
||||
active: compactRoot.containsMouse
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
onPressed: function(mouse) {
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
root.toggleLight()
|
||||
} else {
|
||||
wasExpanded = root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.expanded = !wasExpanded
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.components as PC3
|
||||
|
||||
ColumnLayout {
|
||||
id: fullRoot
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 18
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 10
|
||||
Layout.minimumWidth: Kirigami.Units.gridUnit * 14
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
// Not configured state
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
text: "Run 'elgato discover' or configure host in widget settings"
|
||||
visible: root.lightHost === ""
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Unreachable state
|
||||
PC3.Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
text: "Light unreachable (" + root.lightHost + ")"
|
||||
visible: root.lightHost !== "" && !root.lightReachable
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Controls (only when reachable)
|
||||
ColumnLayout {
|
||||
visible: root.lightHost !== "" && root.lightReachable
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Layout.margins: Kirigami.Units.smallSpacing
|
||||
|
||||
// Header: Power switch
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "video-television"
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
text: "Elgato Key Light"
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.Switch {
|
||||
checked: root.lightOn
|
||||
onToggled: root.toggleLight()
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Brightness slider
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "brightness-high"
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
|
||||
PC3.Slider {
|
||||
id: brightnessSlider
|
||||
Layout.fillWidth: true
|
||||
from: 3
|
||||
to: 100
|
||||
stepSize: 1
|
||||
value: root.lightBrightness
|
||||
onMoved: root.setBrightness(Math.round(value))
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
text: Math.round(brightnessSlider.value) + "%"
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature slider
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "color-management"
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
|
||||
PC3.Slider {
|
||||
id: temperatureSlider
|
||||
Layout.fillWidth: true
|
||||
from: 143
|
||||
to: 344
|
||||
stepSize: 1
|
||||
value: root.lightTemperature
|
||||
onMoved: root.setTemperature(Math.round(value))
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
text: Math.round(1000000 / temperatureSlider.value) + "K"
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 3
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import org.kde.plasma.plasmoid
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
|
||||
PlasmoidItem {
|
||||
id: root
|
||||
|
||||
// State
|
||||
property bool lightOn: false
|
||||
property int lightBrightness: 50
|
||||
property int lightTemperature: 200 // mired
|
||||
property bool lightReachable: false
|
||||
property string lightHost: ""
|
||||
property int lightPort: 9123
|
||||
|
||||
// Resolve host: widget config > shared CLI config
|
||||
function resolveHost() {
|
||||
var cfgHost = Plasmoid.configuration.lightHost || ""
|
||||
var cfgPort = Plasmoid.configuration.lightPort || 9123
|
||||
|
||||
if (cfgHost !== "") {
|
||||
root.lightHost = cfgHost
|
||||
root.lightPort = cfgPort
|
||||
return
|
||||
}
|
||||
|
||||
// Try reading shared CLI config
|
||||
var xhr = new XMLHttpRequest()
|
||||
var configPath = StandardPaths.writableLocation(StandardPaths.GenericConfigLocation) + "/../elgato-cli/config.json"
|
||||
// Use home dir approach
|
||||
try {
|
||||
xhr.open("GET", "file://" + StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.config/elgato-cli/config.json", false)
|
||||
xhr.send()
|
||||
if (xhr.status === 200 || xhr.readyState === XMLHttpRequest.DONE) {
|
||||
var config = JSON.parse(xhr.responseText)
|
||||
if (config.host) {
|
||||
root.lightHost = config.host
|
||||
root.lightPort = config.port || 9123
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Elgato: Could not read shared config:", e)
|
||||
}
|
||||
}
|
||||
|
||||
function baseUrl() {
|
||||
return "http://" + root.lightHost + ":" + root.lightPort + "/elgato"
|
||||
}
|
||||
|
||||
function fetchState() {
|
||||
if (root.lightHost === "") return
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open("GET", baseUrl() + "/lights")
|
||||
xhr.timeout = 3000
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return
|
||||
if (xhr.status === 200) {
|
||||
var data = JSON.parse(xhr.responseText)
|
||||
var light = data.lights[0]
|
||||
root.lightOn = light.on === 1
|
||||
root.lightBrightness = light.brightness
|
||||
root.lightTemperature = light.temperature
|
||||
root.lightReachable = true
|
||||
} else {
|
||||
root.lightReachable = false
|
||||
}
|
||||
}
|
||||
xhr.onerror = function() { root.lightReachable = false }
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
function setLightState(payload) {
|
||||
if (root.lightHost === "") return
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open("PUT", baseUrl() + "/lights")
|
||||
xhr.setRequestHeader("Content-Type", "application/json")
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return
|
||||
if (xhr.status === 200) {
|
||||
var data = JSON.parse(xhr.responseText)
|
||||
var light = data.lights[0]
|
||||
root.lightOn = light.on === 1
|
||||
root.lightBrightness = light.brightness
|
||||
root.lightTemperature = light.temperature
|
||||
root.lightReachable = true
|
||||
} else {
|
||||
root.lightReachable = false
|
||||
}
|
||||
}
|
||||
xhr.onerror = function() { root.lightReachable = false }
|
||||
xhr.send(JSON.stringify({numberOfLights: 1, lights: [payload]}))
|
||||
}
|
||||
|
||||
function toggleLight() {
|
||||
setLightState({
|
||||
on: root.lightOn ? 0 : 1,
|
||||
brightness: root.lightBrightness,
|
||||
temperature: root.lightTemperature
|
||||
})
|
||||
}
|
||||
|
||||
function setBrightness(val) {
|
||||
setLightState({
|
||||
on: root.lightOn ? 1 : 0,
|
||||
brightness: val,
|
||||
temperature: root.lightTemperature
|
||||
})
|
||||
}
|
||||
|
||||
function setTemperature(val) {
|
||||
setLightState({
|
||||
on: root.lightOn ? 1 : 0,
|
||||
brightness: root.lightBrightness,
|
||||
temperature: val
|
||||
})
|
||||
}
|
||||
|
||||
// Polling timer
|
||||
Timer {
|
||||
id: pollTimer
|
||||
interval: Plasmoid.configuration.pollInterval || 3000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.fetchState()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
resolveHost()
|
||||
fetchState()
|
||||
}
|
||||
|
||||
// Tray icon
|
||||
compactRepresentation: CompactRepresentation {}
|
||||
|
||||
// Popup
|
||||
fullRepresentation: FullRepresentation {}
|
||||
|
||||
// Prefer status area
|
||||
Plasmoid.status: root.lightReachable ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus
|
||||
preferredRepresentation: compactRepresentation
|
||||
toolTipMainText: "Elgato Key Light"
|
||||
toolTipSubText: {
|
||||
if (root.lightHost === "") return "Not configured"
|
||||
if (!root.lightReachable) return "Unreachable"
|
||||
return (root.lightOn ? "On" : "Off") + " - " + root.lightBrightness + "% - " + Math.round(1000000 / root.lightTemperature) + "K"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"KPlugin": {
|
||||
"Id": "at.sub-net.elgato",
|
||||
"Name": "Elgato Key Light",
|
||||
"Description": "Control an Elgato Key Light from the system tray",
|
||||
"Category": "Hardware",
|
||||
"Icon": "video-television",
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "Tina Schellander",
|
||||
"Email": "tina@sub-net.at"
|
||||
}
|
||||
],
|
||||
"Version": "0.1.0",
|
||||
"License": "MIT"
|
||||
},
|
||||
"X-Plasma-API": "declarativeappletscript",
|
||||
"X-Plasma-API-Minimum-Version": "6.0",
|
||||
"X-Plasma-Provides": [
|
||||
"org.kde.plasma.systemtray"
|
||||
],
|
||||
"KPackageStructure": "Plasma/Applet"
|
||||
}
|
||||
Reference in New Issue
Block a user