-
-
Notifications
You must be signed in to change notification settings - Fork 69
feat: add MQTT remote control with Home Assistant auto-discovery #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
59f6f3c
02a6e41
cfe9498
727e0e0
79951a7
c41d671
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| name: Build and publish Docker image | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
|
|
||
| env: | ||
| REGISTRY: ghcr.io | ||
| IMAGE_NAME: ${{ github.repository }} | ||
|
|
||
| jobs: | ||
| build-and-push: | ||
| concurrency: | ||
| group: docker-publish-${{ github.ref }} | ||
| cancel-in-progress: true | ||
| runs-on: ubuntu-latest | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Log in to GitHub Container Registry | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ${{ env.REGISTRY }} | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Extract metadata (tags, labels) | ||
| id: meta | ||
| uses: docker/metadata-action@v5 | ||
| with: | ||
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||
| tags: | | ||
| type=ref,event=branch | ||
| type=semver,pattern={{version}} | ||
| type=semver,pattern={{major}}.{{minor}} | ||
| type=raw,value=latest,enable={{is_default_branch}} | ||
|
|
||
| - name: Set up QEMU (for multi-platform builds) | ||
| uses: docker/setup-qemu-action@v3 | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Build and push | ||
| uses: docker/build-push-action@v6 | ||
| with: | ||
| context: . | ||
| platforms: linux/amd64,linux/arm64 | ||
| push: true | ||
| tags: ${{ steps.meta.outputs.tags }} | ||
| labels: ${{ steps.meta.outputs.labels }} | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
| build-args: | | ||
| VERSION=${{ github.sha }} | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * mqtt-sse.ts | ||||||||||||||||||||||||||
| * Listens to the /events SSE endpoint and translates incoming MQTT navigation | ||||||||||||||||||||||||||
| * commands into the HTMX events that the kiosk already understands. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import htmx from "htmx.org"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| type KioskCommand = "next" | "previous"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Initialises the SSE connection to /events. | ||||||||||||||||||||||||||
| * Must be called after the kiosk element is available in the DOM. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export function initMqttSSE(): void { | ||||||||||||||||||||||||||
| const params = new URLSearchParams(window.location.search); | ||||||||||||||||||||||||||
| const url = params.toString() ? `/events?${params.toString()}` : "/events"; | ||||||||||||||||||||||||||
| const evtSource = new EventSource(url); | ||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| evtSource.addEventListener("kiosk-command", (e: MessageEvent) => { | ||||||||||||||||||||||||||
| const cmd = (e.data as string).trim() as KioskCommand; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| switch (cmd) { | ||||||||||||||||||||||||||
| case "next": { | ||||||||||||||||||||||||||
| const kioskEl = document.getElementById("kiosk"); | ||||||||||||||||||||||||||
| if (kioskEl) { | ||||||||||||||||||||||||||
| htmx.trigger(kioskEl, "kiosk-new-asset"); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| case "previous": { | ||||||||||||||||||||||||||
| const prevEl = document.getElementById( | ||||||||||||||||||||||||||
| "navigation-interaction-area--previous-asset", | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| if (prevEl) { | ||||||||||||||||||||||||||
| htmx.trigger(prevEl, "kiosk-prev-asset"); | ||||||||||||||||||||||||||
|
Comment on lines
+31
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
↩️ Minimal fix- htmx.trigger(prevEl, "kiosk-prev-asset");
+ htmx.trigger(prevEl, "click");📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||
| console.warn("immich-kiosk: unknown MQTT command:", cmd); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| evtSource.onerror = () => { | ||||||||||||||||||||||||||
| // Browser will automatically reconnect for EventSource; nothing extra needed. | ||||||||||||||||||||||||||
| console.debug("immich-kiosk: SSE connection error, will retry automatically"); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -144,6 +144,22 @@ type KioskSettings struct { | |
| DebugVerbose bool `json:"debugVerbose" yaml:"debug_verbose" mapstructure:"debug_verbose" default:"false"` | ||
|
|
||
| DemoMode bool `json:"-" yaml:"-" mapstructure:"demo_mode" default:"false"` | ||
|
|
||
| // MQTT settings | ||
| // MqttEnabled enables MQTT support for remote control (e.g. from Home Assistant) | ||
| MqttEnabled bool `json:"mqttEnabled" yaml:"mqtt_enabled" mapstructure:"mqtt_enabled" default:"false"` | ||
| // MqttBroker is the hostname or IP address of the MQTT broker | ||
| MqttBroker string `json:"mqttBroker" yaml:"mqtt_broker" mapstructure:"mqtt_broker" default:""` | ||
| // MqttPort is the port of the MQTT broker | ||
| MqttPort int `json:"mqttPort" yaml:"mqtt_port" mapstructure:"mqtt_port" default:"1883"` | ||
| // MqttUsername is the MQTT broker username (optional) | ||
| MqttUsername string `json:"mqttUsername" yaml:"mqtt_username" mapstructure:"mqtt_username" default:""` | ||
| // MqttPassword is the MQTT broker password (optional) | ||
| MqttPassword string `json:"-" yaml:"mqtt_password" mapstructure:"mqtt_password" default:"" redact:"true"` | ||
| // MqttClientID is the MQTT client identifier | ||
| MqttClientID string `json:"mqttClientId" yaml:"mqtt_client_id" mapstructure:"mqtt_client_id" default:"immich-kiosk"` | ||
| // MqttTopicPrefix is the MQTT topic prefix. Commands are received on <prefix>/command | ||
| MqttTopicPrefix string `json:"mqttTopicPrefix" yaml:"mqtt_topic_prefix" mapstructure:"mqtt_topic_prefix" default:"immich-kiosk"` | ||
|
Comment on lines
+159
to
+162
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In MQTT, client IDs must be unique per broker. When two clients attempt to connect using the same client ID, the broker drops the earlier (existing) connection and accepts the new one. This behavior is mandated by the MQTT specifications for both versions 3.1.1 and 5.0 to ensure session integrity and allow session recovery after network failures. For MQTT 3.1.1: If the ClientId represents a Client already connected to the Server then the Server MUST disconnect the existing Client [MQTT-3.1.4-2] (referenced across multiple sources including broker logs and discussions). For MQTT 5.0: If the ClientID represents a Client already connected to the Server, the Server sends a DISCONNECT packet to the existing Client with Reason Code 0x8E (Session taken over) and MUST close the Network Connection of the existing Client [MQTT-3.1.4-3]. This can lead to connection flapping if both clients auto-reconnect. Some brokers offer configuration options (e.g., Mosquitto's allow_duplicate_clients) to reject duplicates instead, but standard compliant behavior is to drop the older connection. Always use unique client IDs, such as by appending random suffixes or UUIDs. Citations:
🏁 Script executed: # Search for MqttClientID usage across the codebase
rg "MqttClientID" -A 3 -B 3
# Also check for any UUID or unique ID generation logic related to MQTT
rg -i "mqtt.*id|clientid" --type go -A 2 | head -100Repository: damongolding/immich-kiosk Length of output: 3637 🏁 Script executed: # Check for initialization/startup code that might modify MqttClientID
rg -i "hostname|uuid|machine.*id" --type go internal/ | head -50
# Also check main.go or any startup sequence
find . -name "main.go" -o -name "init.go" | xargs ls -la 2>/dev/null
# Look for any config post-processing that might add uniqueness
rg "MqttClientID\s*=" --type go -A 5 -B 5Repository: damongolding/immich-kiosk Length of output: 4557 🏁 Script executed: # Check main.go to see the startup sequence
head -100 main.go
# Look for where config is loaded and if it's post-processed
rg "NewConfig|LoadConfig" --type go -A 10 -B 2
# Check if KioskSettings or MqttClientID is modified after config load
rg "MqttClientID\s*=" --type go -B 5 -A 5 | grep -v "default:"Repository: damongolding/immich-kiosk Length of output: 2732 🏁 Script executed: # Check the config Load method to see if it modifies MqttClientID
rg "func.*Load" internal/config/ --type go -A 15 | head -80
# Check mqtt.New() function
rg "func.*New.*Client|type Client struct" internal/mqtt/ --type go -A 10Repository: damongolding/immich-kiosk Length of output: 3240 Use a unique identifier for the MQTT client ID instead of a fixed default. Multiple kiosk instances using the default 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| type WeatherConfig struct { | ||
|
|
@@ -537,6 +553,13 @@ func bindEnvironmentVariables(v *viper.Viper) error { | |
| {"kiosk.debug_verbose", "KIOSK_DEBUG_VERBOSE"}, | ||
| {"kiosk.demo_mode", "KIOSK_DEMO_MODE"}, | ||
| {"kiosk.config_validation_level", "KIOSK_CONFIG_VALIDATION_LEVEL"}, | ||
| {"kiosk.mqtt_enabled", "KIOSK_MQTT_ENABLED"}, | ||
| {"kiosk.mqtt_broker", "KIOSK_MQTT_BROKER"}, | ||
| {"kiosk.mqtt_port", "KIOSK_MQTT_PORT"}, | ||
| {"kiosk.mqtt_username", "KIOSK_MQTT_USERNAME"}, | ||
| {"kiosk.mqtt_password", "KIOSK_MQTT_PASSWORD"}, | ||
| {"kiosk.mqtt_client_id", "KIOSK_MQTT_CLIENT_ID"}, | ||
| {"kiosk.mqtt_topic_prefix", "KIOSK_MQTT_TOPIC_PREFIX"}, | ||
| {"offline_mode.enabled", "KIOSK_OFFLINE_MODE_ENABLED"}, | ||
| {"offline_mode.number_of_assets", "KIOSK_OFFLINE_MODE_NUMBER_OF_ASSETS"}, | ||
| {"offline_mode.max_size", "KIOSK_OFFLINE_MODE_MAX_SIZE"}, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.