From 9b839839e3658e6dbf82a5274335d1372995f108 Mon Sep 17 00:00:00 2001 From: FiestaBoard CI Date: Sat, 24 Oct 2026 18:43:26 +0000 Subject: [PATCH] docs: snapshot version 9.14 --- docs/.source-sha | 2 +- tests/baseline.json | 2924 ++--------------- .../version-9.14/deployment/_category_.json | 8 + .../version-9.14/deployment/fiestaupdater.md | 270 ++ .../version-9.14/deployment/production.md | 145 + .../version-9.14/deployment/raspberry-pi.md | 120 + .../version-9.14/development/_category_.json | 8 + .../version-9.14/development/contributing.md | 109 + .../version-9.14/development/plugin-ai.md | 136 + .../version-9.14/development/plugin-guide.md | 1138 +++++++ .../version-9.14/development/plugin-oauth.md | 566 ++++ .../version-9.14/development/testing.md | 161 + .../version-9.14/features/_category_.json | 8 + .../features/connecting-accounts.md | 126 + .../version-9.14/features/fiestapanel.md | 86 + .../features/home-assistant-control.md | 556 ++++ .../version-9.14/features/localization.md | 80 + .../version-9.14/features/page-editor.md | 235 ++ .../version-9.14/features/schedule.md | 116 + .../version-9.14/features/silence-schedule.md | 105 + .../version-9.14/features/transitions.md | 146 + .../version-9.14/features/updating.md | 199 ++ versioned_docs/version-9.14/intro.md | 123 + .../version-9.14/reference/_category_.json | 8 + .../version-9.14/reference/accessibility.md | 40 + .../version-9.14/reference/api-endpoints.md | 2006 +++++++++++ .../version-9.14/reference/character-codes.md | 142 + .../version-9.14/reference/color-guide.md | 91 + .../reference/environment-variables.md | 168 + .../reference/template-formulas.md | 535 +++ .../version-9.14/setup/_category_.json | 8 + .../version-9.14/setup/ai-providers.md | 329 ++ versioned_docs/version-9.14/setup/api-keys.md | 82 + .../version-9.14/setup/authentication.md | 193 ++ .../version-9.14/setup/beginners-guide.md | 334 ++ .../version-9.14/setup/cloud-api.md | 91 + .../version-9.14/setup/docker-setup.md | 186 ++ .../version-9.14/setup/first-10-minutes.md | 169 + .../setup/home-assistant-addon.md | 88 + .../version-9.14/setup/home-assistant-mqtt.md | 262 ++ .../version-9.14/setup/local-development.md | 71 + .../setup/local-home-assistant.md | 93 + .../version-9.14/setup/note-arrays.md | 143 + .../version-9.14/setup/quick-start.md | 154 + .../version-9.14/setup/raspberry-pi.md | 139 + .../version-9.14/setup/v2-migration.md | 368 +++ .../version-9.14/setup/v3-migration.md | 229 ++ .../version-9.14/troubleshooting.md | 282 ++ versioned_sidebars/version-9.14-sidebars.json | 70 + versions.json | 1 + 50 files changed, 10998 insertions(+), 2651 deletions(-) create mode 100644 versioned_docs/version-9.14/deployment/_category_.json create mode 100644 versioned_docs/version-9.14/deployment/fiestaupdater.md create mode 100644 versioned_docs/version-9.14/deployment/production.md create mode 100644 versioned_docs/version-9.14/deployment/raspberry-pi.md create mode 100644 versioned_docs/version-9.14/development/_category_.json create mode 100644 versioned_docs/version-9.14/development/contributing.md create mode 100644 versioned_docs/version-9.14/development/plugin-ai.md create mode 100644 versioned_docs/version-9.14/development/plugin-guide.md create mode 100644 versioned_docs/version-9.14/development/plugin-oauth.md create mode 100644 versioned_docs/version-9.14/development/testing.md create mode 100644 versioned_docs/version-9.14/features/_category_.json create mode 100644 versioned_docs/version-9.14/features/connecting-accounts.md create mode 100644 versioned_docs/version-9.14/features/fiestapanel.md create mode 100644 versioned_docs/version-9.14/features/home-assistant-control.md create mode 100644 versioned_docs/version-9.14/features/localization.md create mode 100644 versioned_docs/version-9.14/features/page-editor.md create mode 100644 versioned_docs/version-9.14/features/schedule.md create mode 100644 versioned_docs/version-9.14/features/silence-schedule.md create mode 100644 versioned_docs/version-9.14/features/transitions.md create mode 100644 versioned_docs/version-9.14/features/updating.md create mode 100644 versioned_docs/version-9.14/intro.md create mode 100644 versioned_docs/version-9.14/reference/_category_.json create mode 100644 versioned_docs/version-9.14/reference/accessibility.md create mode 100644 versioned_docs/version-9.14/reference/api-endpoints.md create mode 100644 versioned_docs/version-9.14/reference/character-codes.md create mode 100644 versioned_docs/version-9.14/reference/color-guide.md create mode 100644 versioned_docs/version-9.14/reference/environment-variables.md create mode 100644 versioned_docs/version-9.14/reference/template-formulas.md create mode 100644 versioned_docs/version-9.14/setup/_category_.json create mode 100644 versioned_docs/version-9.14/setup/ai-providers.md create mode 100644 versioned_docs/version-9.14/setup/api-keys.md create mode 100644 versioned_docs/version-9.14/setup/authentication.md create mode 100644 versioned_docs/version-9.14/setup/beginners-guide.md create mode 100644 versioned_docs/version-9.14/setup/cloud-api.md create mode 100644 versioned_docs/version-9.14/setup/docker-setup.md create mode 100644 versioned_docs/version-9.14/setup/first-10-minutes.md create mode 100644 versioned_docs/version-9.14/setup/home-assistant-addon.md create mode 100644 versioned_docs/version-9.14/setup/home-assistant-mqtt.md create mode 100644 versioned_docs/version-9.14/setup/local-development.md create mode 100644 versioned_docs/version-9.14/setup/local-home-assistant.md create mode 100644 versioned_docs/version-9.14/setup/note-arrays.md create mode 100644 versioned_docs/version-9.14/setup/quick-start.md create mode 100644 versioned_docs/version-9.14/setup/raspberry-pi.md create mode 100644 versioned_docs/version-9.14/setup/v2-migration.md create mode 100644 versioned_docs/version-9.14/setup/v3-migration.md create mode 100644 versioned_docs/version-9.14/troubleshooting.md create mode 100644 versioned_sidebars/version-9.14-sidebars.json diff --git a/docs/.source-sha b/docs/.source-sha index 5772332a72..ab57ce7e0d 100644 --- a/docs/.source-sha +++ b/docs/.source-sha @@ -1 +1 @@ -8cf296b6e2ffca9fca408ea86b9997c62811fa9a +94692dceffda0205be5d7c829f97aaeba61defe6 diff --git a/tests/baseline.json b/tests/baseline.json index 08f7dafeb9..80e1576440 100644 --- a/tests/baseline.json +++ b/tests/baseline.json @@ -12,69 +12,6 @@ "/blog/tags/release", "/blog/wysiwyg-editor-scheduling-disney-parks", "/buyavestaboard", - "/docs/8.29/deployment/fiestaupdater", - "/docs/8.29/deployment/production", - "/docs/8.29/deployment/raspberry-pi", - "/docs/8.29/development/contributing", - "/docs/8.29/development/plugin-guide", - "/docs/8.29/development/testing", - "/docs/8.29/features/home-assistant-control", - "/docs/8.29/features/localization", - "/docs/8.29/features/page-editor", - "/docs/8.29/features/schedule", - "/docs/8.29/features/silence-schedule", - "/docs/8.29/features/transitions", - "/docs/8.29/features/updating", - "/docs/8.29/intro", - "/docs/8.29/plugins/air-fog", - "/docs/8.29/plugins/baywheels", - "/docs/8.29/plugins/configuration", - "/docs/8.29/plugins/countdown", - "/docs/8.29/plugins/date-time", - "/docs/8.29/plugins/disney-parks", - "/docs/8.29/plugins/entertainment", - "/docs/8.29/plugins/guest-wifi", - "/docs/8.29/plugins/home-assistant", - "/docs/8.29/plugins/last-fm", - "/docs/8.29/plugins/muni", - "/docs/8.29/plugins/nearby-aircraft", - "/docs/8.29/plugins/overview", - "/docs/8.29/plugins/random", - "/docs/8.29/plugins/sports-scores", - "/docs/8.29/plugins/star-trek-quotes", - "/docs/8.29/plugins/stardate", - "/docs/8.29/plugins/stocks", - "/docs/8.29/plugins/sun-art", - "/docs/8.29/plugins/surf", - "/docs/8.29/plugins/traffic", - "/docs/8.29/plugins/transit", - "/docs/8.29/plugins/visual-clock", - "/docs/8.29/plugins/weather", - "/docs/8.29/plugins/wsdot", - "/docs/8.29/reference/accessibility", - "/docs/8.29/reference/api-endpoints", - "/docs/8.29/reference/character-codes", - "/docs/8.29/reference/color-guide", - "/docs/8.29/reference/environment-variables", - "/docs/8.29/reference/template-formulas", - "/docs/8.29/setup/ai-providers", - "/docs/8.29/setup/api-keys", - "/docs/8.29/setup/authentication", - "/docs/8.29/setup/beginners-guide", - "/docs/8.29/setup/cloud-api", - "/docs/8.29/setup/docker-setup", - "/docs/8.29/setup/first-10-minutes", - "/docs/8.29/setup/home-assistant-addon", - "/docs/8.29/setup/home-assistant-mqtt", - "/docs/8.29/setup/local-development", - "/docs/8.29/setup/local-home-assistant", - "/docs/8.29/setup/note-arrays", - "/docs/8.29/setup/quick-start", - "/docs/8.29/setup/raspberry-pi", - "/docs/8.29/setup/secret-encryption", - "/docs/8.29/setup/v2-migration", - "/docs/8.29/setup/v3-migration", - "/docs/8.29/troubleshooting", "/docs/8.30/deployment/fiestaupdater", "/docs/8.30/deployment/production", "/docs/8.30/deployment/raspberry-pi", @@ -648,6 +585,74 @@ "/docs/8.38/setup/v2-migration", "/docs/8.38/setup/v3-migration", "/docs/8.38/troubleshooting", + "/docs/9.10/deployment/fiestaupdater", + "/docs/9.10/deployment/production", + "/docs/9.10/deployment/raspberry-pi", + "/docs/9.10/development/contributing", + "/docs/9.10/development/plugin-guide", + "/docs/9.10/development/plugin-oauth", + "/docs/9.10/development/testing", + "/docs/9.10/features/connecting-accounts", + "/docs/9.10/features/fiestapanel", + "/docs/9.10/features/home-assistant-control", + "/docs/9.10/features/localization", + "/docs/9.10/features/page-editor", + "/docs/9.10/features/schedule", + "/docs/9.10/features/silence-schedule", + "/docs/9.10/features/transitions", + "/docs/9.10/features/updating", + "/docs/9.10/intro", + "/docs/9.10/plugins/air-fog", + "/docs/9.10/plugins/baywheels", + "/docs/9.10/plugins/configuration", + "/docs/9.10/plugins/countdown", + "/docs/9.10/plugins/date-time", + "/docs/9.10/plugins/disney-parks", + "/docs/9.10/plugins/entertainment", + "/docs/9.10/plugins/guest-wifi", + "/docs/9.10/plugins/home-assistant", + "/docs/9.10/plugins/last-fm", + "/docs/9.10/plugins/muni", + "/docs/9.10/plugins/nearby-aircraft", + "/docs/9.10/plugins/overview", + "/docs/9.10/plugins/plex", + "/docs/9.10/plugins/random", + "/docs/9.10/plugins/shopify", + "/docs/9.10/plugins/sports-scores", + "/docs/9.10/plugins/spotify", + "/docs/9.10/plugins/star-trek-quotes", + "/docs/9.10/plugins/stardate", + "/docs/9.10/plugins/stocks", + "/docs/9.10/plugins/sun-art", + "/docs/9.10/plugins/surf", + "/docs/9.10/plugins/traffic", + "/docs/9.10/plugins/transit", + "/docs/9.10/plugins/visual-clock", + "/docs/9.10/plugins/weather", + "/docs/9.10/plugins/wsdot", + "/docs/9.10/reference/accessibility", + "/docs/9.10/reference/api-endpoints", + "/docs/9.10/reference/character-codes", + "/docs/9.10/reference/color-guide", + "/docs/9.10/reference/environment-variables", + "/docs/9.10/reference/template-formulas", + "/docs/9.10/setup/ai-providers", + "/docs/9.10/setup/api-keys", + "/docs/9.10/setup/authentication", + "/docs/9.10/setup/beginners-guide", + "/docs/9.10/setup/cloud-api", + "/docs/9.10/setup/docker-setup", + "/docs/9.10/setup/first-10-minutes", + "/docs/9.10/setup/home-assistant-addon", + "/docs/9.10/setup/home-assistant-mqtt", + "/docs/9.10/setup/local-development", + "/docs/9.10/setup/local-home-assistant", + "/docs/9.10/setup/note-arrays", + "/docs/9.10/setup/quick-start", + "/docs/9.10/setup/raspberry-pi", + "/docs/9.10/setup/v2-migration", + "/docs/9.10/setup/v3-migration", + "/docs/9.10/troubleshooting", "/docs/9.8/deployment/fiestaupdater", "/docs/9.8/deployment/production", "/docs/9.8/deployment/raspberry-pi", @@ -720,6 +725,7 @@ "/docs/deployment/production", "/docs/deployment/raspberry-pi", "/docs/development/contributing", + "/docs/development/plugin-ai", "/docs/development/plugin-guide", "/docs/development/plugin-oauth", "/docs/development/testing", @@ -733,34 +739,6 @@ "/docs/features/transitions", "/docs/features/updating", "/docs/intro", - "/docs/plugins/air-fog", - "/docs/plugins/baywheels", - "/docs/plugins/configuration", - "/docs/plugins/countdown", - "/docs/plugins/date-time", - "/docs/plugins/disney-parks", - "/docs/plugins/entertainment", - "/docs/plugins/guest-wifi", - "/docs/plugins/home-assistant", - "/docs/plugins/last-fm", - "/docs/plugins/muni", - "/docs/plugins/nearby-aircraft", - "/docs/plugins/overview", - "/docs/plugins/plex", - "/docs/plugins/random", - "/docs/plugins/shopify", - "/docs/plugins/sports-scores", - "/docs/plugins/spotify", - "/docs/plugins/star-trek-quotes", - "/docs/plugins/stardate", - "/docs/plugins/stocks", - "/docs/plugins/sun-art", - "/docs/plugins/surf", - "/docs/plugins/traffic", - "/docs/plugins/transit", - "/docs/plugins/visual-clock", - "/docs/plugins/weather", - "/docs/plugins/wsdot", "/docs/reference/accessibility", "/docs/reference/api-endpoints", "/docs/reference/character-codes", @@ -797,6 +775,7 @@ "/docs/2.10/deployment/production": "/docs/deployment/production", "/docs/2.10/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/2.10/development/contributing": "/docs/development/contributing", + "/docs/2.10/development/plugin-ai": "/docs/development/plugin-ai", "/docs/2.10/development/plugin-guide": "/docs/development/plugin-guide", "/docs/2.10/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/2.10/development/testing": "/docs/development/testing", @@ -810,34 +789,6 @@ "/docs/2.10/features/transitions": "/docs/features/transitions", "/docs/2.10/features/updating": "/docs/features/updating", "/docs/2.10/intro": "/docs/intro", - "/docs/2.10/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/2.10/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/2.10/plugins/configuration": "/docs/plugins/configuration", - "/docs/2.10/plugins/countdown": "/docs/plugins/countdown", - "/docs/2.10/plugins/date-time": "/docs/plugins/date-time", - "/docs/2.10/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/2.10/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/2.10/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/2.10/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/2.10/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/2.10/plugins/muni": "/docs/plugins/muni", - "/docs/2.10/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/2.10/plugins/overview": "/docs/plugins/overview", - "/docs/2.10/plugins/plex": "/docs/plugins/plex", - "/docs/2.10/plugins/random": "/docs/plugins/random", - "/docs/2.10/plugins/shopify": "/docs/plugins/shopify", - "/docs/2.10/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/2.10/plugins/spotify": "/docs/plugins/spotify", - "/docs/2.10/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/2.10/plugins/stardate": "/docs/plugins/stardate", - "/docs/2.10/plugins/stocks": "/docs/plugins/stocks", - "/docs/2.10/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/2.10/plugins/surf": "/docs/plugins/surf", - "/docs/2.10/plugins/traffic": "/docs/plugins/traffic", - "/docs/2.10/plugins/transit": "/docs/plugins/transit", - "/docs/2.10/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/2.10/plugins/weather": "/docs/plugins/weather", - "/docs/2.10/plugins/wsdot": "/docs/plugins/wsdot", "/docs/2.10/reference/accessibility": "/docs/reference/accessibility", "/docs/2.10/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/2.10/reference/character-codes": "/docs/reference/character-codes", @@ -866,6 +817,7 @@ "/docs/2.11/deployment/production": "/docs/deployment/production", "/docs/2.11/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/2.11/development/contributing": "/docs/development/contributing", + "/docs/2.11/development/plugin-ai": "/docs/development/plugin-ai", "/docs/2.11/development/plugin-guide": "/docs/development/plugin-guide", "/docs/2.11/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/2.11/development/testing": "/docs/development/testing", @@ -879,34 +831,6 @@ "/docs/2.11/features/transitions": "/docs/features/transitions", "/docs/2.11/features/updating": "/docs/features/updating", "/docs/2.11/intro": "/docs/intro", - "/docs/2.11/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/2.11/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/2.11/plugins/configuration": "/docs/plugins/configuration", - "/docs/2.11/plugins/countdown": "/docs/plugins/countdown", - "/docs/2.11/plugins/date-time": "/docs/plugins/date-time", - "/docs/2.11/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/2.11/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/2.11/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/2.11/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/2.11/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/2.11/plugins/muni": "/docs/plugins/muni", - "/docs/2.11/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/2.11/plugins/overview": "/docs/plugins/overview", - "/docs/2.11/plugins/plex": "/docs/plugins/plex", - "/docs/2.11/plugins/random": "/docs/plugins/random", - "/docs/2.11/plugins/shopify": "/docs/plugins/shopify", - "/docs/2.11/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/2.11/plugins/spotify": "/docs/plugins/spotify", - "/docs/2.11/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/2.11/plugins/stardate": "/docs/plugins/stardate", - "/docs/2.11/plugins/stocks": "/docs/plugins/stocks", - "/docs/2.11/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/2.11/plugins/surf": "/docs/plugins/surf", - "/docs/2.11/plugins/traffic": "/docs/plugins/traffic", - "/docs/2.11/plugins/transit": "/docs/plugins/transit", - "/docs/2.11/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/2.11/plugins/weather": "/docs/plugins/weather", - "/docs/2.11/plugins/wsdot": "/docs/plugins/wsdot", "/docs/2.11/reference/accessibility": "/docs/reference/accessibility", "/docs/2.11/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/2.11/reference/character-codes": "/docs/reference/character-codes", @@ -935,6 +859,7 @@ "/docs/2.5/deployment/production": "/docs/deployment/production", "/docs/2.5/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/2.5/development/contributing": "/docs/development/contributing", + "/docs/2.5/development/plugin-ai": "/docs/development/plugin-ai", "/docs/2.5/development/plugin-guide": "/docs/development/plugin-guide", "/docs/2.5/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/2.5/development/testing": "/docs/development/testing", @@ -948,34 +873,6 @@ "/docs/2.5/features/transitions": "/docs/features/transitions", "/docs/2.5/features/updating": "/docs/features/updating", "/docs/2.5/intro": "/docs/intro", - "/docs/2.5/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/2.5/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/2.5/plugins/configuration": "/docs/plugins/configuration", - "/docs/2.5/plugins/countdown": "/docs/plugins/countdown", - "/docs/2.5/plugins/date-time": "/docs/plugins/date-time", - "/docs/2.5/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/2.5/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/2.5/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/2.5/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/2.5/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/2.5/plugins/muni": "/docs/plugins/muni", - "/docs/2.5/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/2.5/plugins/overview": "/docs/plugins/overview", - "/docs/2.5/plugins/plex": "/docs/plugins/plex", - "/docs/2.5/plugins/random": "/docs/plugins/random", - "/docs/2.5/plugins/shopify": "/docs/plugins/shopify", - "/docs/2.5/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/2.5/plugins/spotify": "/docs/plugins/spotify", - "/docs/2.5/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/2.5/plugins/stardate": "/docs/plugins/stardate", - "/docs/2.5/plugins/stocks": "/docs/plugins/stocks", - "/docs/2.5/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/2.5/plugins/surf": "/docs/plugins/surf", - "/docs/2.5/plugins/traffic": "/docs/plugins/traffic", - "/docs/2.5/plugins/transit": "/docs/plugins/transit", - "/docs/2.5/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/2.5/plugins/weather": "/docs/plugins/weather", - "/docs/2.5/plugins/wsdot": "/docs/plugins/wsdot", "/docs/2.5/reference/accessibility": "/docs/reference/accessibility", "/docs/2.5/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/2.5/reference/character-codes": "/docs/reference/character-codes", @@ -1004,6 +901,7 @@ "/docs/2.6/deployment/production": "/docs/deployment/production", "/docs/2.6/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/2.6/development/contributing": "/docs/development/contributing", + "/docs/2.6/development/plugin-ai": "/docs/development/plugin-ai", "/docs/2.6/development/plugin-guide": "/docs/development/plugin-guide", "/docs/2.6/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/2.6/development/testing": "/docs/development/testing", @@ -1017,34 +915,6 @@ "/docs/2.6/features/transitions": "/docs/features/transitions", "/docs/2.6/features/updating": "/docs/features/updating", "/docs/2.6/intro": "/docs/intro", - "/docs/2.6/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/2.6/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/2.6/plugins/configuration": "/docs/plugins/configuration", - "/docs/2.6/plugins/countdown": "/docs/plugins/countdown", - "/docs/2.6/plugins/date-time": "/docs/plugins/date-time", - "/docs/2.6/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/2.6/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/2.6/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/2.6/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/2.6/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/2.6/plugins/muni": "/docs/plugins/muni", - "/docs/2.6/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/2.6/plugins/overview": "/docs/plugins/overview", - "/docs/2.6/plugins/plex": "/docs/plugins/plex", - "/docs/2.6/plugins/random": "/docs/plugins/random", - "/docs/2.6/plugins/shopify": "/docs/plugins/shopify", - "/docs/2.6/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/2.6/plugins/spotify": "/docs/plugins/spotify", - "/docs/2.6/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/2.6/plugins/stardate": "/docs/plugins/stardate", - "/docs/2.6/plugins/stocks": "/docs/plugins/stocks", - "/docs/2.6/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/2.6/plugins/surf": "/docs/plugins/surf", - "/docs/2.6/plugins/traffic": "/docs/plugins/traffic", - "/docs/2.6/plugins/transit": "/docs/plugins/transit", - "/docs/2.6/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/2.6/plugins/weather": "/docs/plugins/weather", - "/docs/2.6/plugins/wsdot": "/docs/plugins/wsdot", "/docs/2.6/reference/accessibility": "/docs/reference/accessibility", "/docs/2.6/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/2.6/reference/character-codes": "/docs/reference/character-codes", @@ -1073,6 +943,7 @@ "/docs/2.7/deployment/production": "/docs/deployment/production", "/docs/2.7/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/2.7/development/contributing": "/docs/development/contributing", + "/docs/2.7/development/plugin-ai": "/docs/development/plugin-ai", "/docs/2.7/development/plugin-guide": "/docs/development/plugin-guide", "/docs/2.7/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/2.7/development/testing": "/docs/development/testing", @@ -1086,34 +957,6 @@ "/docs/2.7/features/transitions": "/docs/features/transitions", "/docs/2.7/features/updating": "/docs/features/updating", "/docs/2.7/intro": "/docs/intro", - "/docs/2.7/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/2.7/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/2.7/plugins/configuration": "/docs/plugins/configuration", - "/docs/2.7/plugins/countdown": "/docs/plugins/countdown", - "/docs/2.7/plugins/date-time": "/docs/plugins/date-time", - "/docs/2.7/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/2.7/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/2.7/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/2.7/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/2.7/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/2.7/plugins/muni": "/docs/plugins/muni", - "/docs/2.7/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/2.7/plugins/overview": "/docs/plugins/overview", - "/docs/2.7/plugins/plex": "/docs/plugins/plex", - "/docs/2.7/plugins/random": "/docs/plugins/random", - "/docs/2.7/plugins/shopify": "/docs/plugins/shopify", - "/docs/2.7/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/2.7/plugins/spotify": "/docs/plugins/spotify", - "/docs/2.7/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/2.7/plugins/stardate": "/docs/plugins/stardate", - "/docs/2.7/plugins/stocks": "/docs/plugins/stocks", - "/docs/2.7/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/2.7/plugins/surf": "/docs/plugins/surf", - "/docs/2.7/plugins/traffic": "/docs/plugins/traffic", - "/docs/2.7/plugins/transit": "/docs/plugins/transit", - "/docs/2.7/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/2.7/plugins/weather": "/docs/plugins/weather", - "/docs/2.7/plugins/wsdot": "/docs/plugins/wsdot", "/docs/2.7/reference/accessibility": "/docs/reference/accessibility", "/docs/2.7/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/2.7/reference/character-codes": "/docs/reference/character-codes", @@ -1142,6 +985,7 @@ "/docs/2.8/deployment/production": "/docs/deployment/production", "/docs/2.8/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/2.8/development/contributing": "/docs/development/contributing", + "/docs/2.8/development/plugin-ai": "/docs/development/plugin-ai", "/docs/2.8/development/plugin-guide": "/docs/development/plugin-guide", "/docs/2.8/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/2.8/development/testing": "/docs/development/testing", @@ -1155,34 +999,6 @@ "/docs/2.8/features/transitions": "/docs/features/transitions", "/docs/2.8/features/updating": "/docs/features/updating", "/docs/2.8/intro": "/docs/intro", - "/docs/2.8/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/2.8/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/2.8/plugins/configuration": "/docs/plugins/configuration", - "/docs/2.8/plugins/countdown": "/docs/plugins/countdown", - "/docs/2.8/plugins/date-time": "/docs/plugins/date-time", - "/docs/2.8/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/2.8/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/2.8/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/2.8/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/2.8/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/2.8/plugins/muni": "/docs/plugins/muni", - "/docs/2.8/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/2.8/plugins/overview": "/docs/plugins/overview", - "/docs/2.8/plugins/plex": "/docs/plugins/plex", - "/docs/2.8/plugins/random": "/docs/plugins/random", - "/docs/2.8/plugins/shopify": "/docs/plugins/shopify", - "/docs/2.8/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/2.8/plugins/spotify": "/docs/plugins/spotify", - "/docs/2.8/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/2.8/plugins/stardate": "/docs/plugins/stardate", - "/docs/2.8/plugins/stocks": "/docs/plugins/stocks", - "/docs/2.8/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/2.8/plugins/surf": "/docs/plugins/surf", - "/docs/2.8/plugins/traffic": "/docs/plugins/traffic", - "/docs/2.8/plugins/transit": "/docs/plugins/transit", - "/docs/2.8/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/2.8/plugins/weather": "/docs/plugins/weather", - "/docs/2.8/plugins/wsdot": "/docs/plugins/wsdot", "/docs/2.8/reference/accessibility": "/docs/reference/accessibility", "/docs/2.8/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/2.8/reference/character-codes": "/docs/reference/character-codes", @@ -1211,6 +1027,7 @@ "/docs/2.9/deployment/production": "/docs/deployment/production", "/docs/2.9/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/2.9/development/contributing": "/docs/development/contributing", + "/docs/2.9/development/plugin-ai": "/docs/development/plugin-ai", "/docs/2.9/development/plugin-guide": "/docs/development/plugin-guide", "/docs/2.9/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/2.9/development/testing": "/docs/development/testing", @@ -1224,34 +1041,6 @@ "/docs/2.9/features/transitions": "/docs/features/transitions", "/docs/2.9/features/updating": "/docs/features/updating", "/docs/2.9/intro": "/docs/intro", - "/docs/2.9/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/2.9/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/2.9/plugins/configuration": "/docs/plugins/configuration", - "/docs/2.9/plugins/countdown": "/docs/plugins/countdown", - "/docs/2.9/plugins/date-time": "/docs/plugins/date-time", - "/docs/2.9/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/2.9/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/2.9/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/2.9/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/2.9/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/2.9/plugins/muni": "/docs/plugins/muni", - "/docs/2.9/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/2.9/plugins/overview": "/docs/plugins/overview", - "/docs/2.9/plugins/plex": "/docs/plugins/plex", - "/docs/2.9/plugins/random": "/docs/plugins/random", - "/docs/2.9/plugins/shopify": "/docs/plugins/shopify", - "/docs/2.9/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/2.9/plugins/spotify": "/docs/plugins/spotify", - "/docs/2.9/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/2.9/plugins/stardate": "/docs/plugins/stardate", - "/docs/2.9/plugins/stocks": "/docs/plugins/stocks", - "/docs/2.9/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/2.9/plugins/surf": "/docs/plugins/surf", - "/docs/2.9/plugins/traffic": "/docs/plugins/traffic", - "/docs/2.9/plugins/transit": "/docs/plugins/transit", - "/docs/2.9/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/2.9/plugins/weather": "/docs/plugins/weather", - "/docs/2.9/plugins/wsdot": "/docs/plugins/wsdot", "/docs/2.9/reference/accessibility": "/docs/reference/accessibility", "/docs/2.9/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/2.9/reference/character-codes": "/docs/reference/character-codes", @@ -1280,6 +1069,7 @@ "/docs/4.0/deployment/production": "/docs/deployment/production", "/docs/4.0/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.0/development/contributing": "/docs/development/contributing", + "/docs/4.0/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.0/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.0/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.0/development/testing": "/docs/development/testing", @@ -1293,34 +1083,6 @@ "/docs/4.0/features/transitions": "/docs/features/transitions", "/docs/4.0/features/updating": "/docs/features/updating", "/docs/4.0/intro": "/docs/intro", - "/docs/4.0/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.0/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.0/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.0/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.0/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.0/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.0/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.0/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.0/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.0/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.0/plugins/muni": "/docs/plugins/muni", - "/docs/4.0/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.0/plugins/overview": "/docs/plugins/overview", - "/docs/4.0/plugins/plex": "/docs/plugins/plex", - "/docs/4.0/plugins/random": "/docs/plugins/random", - "/docs/4.0/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.0/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.0/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.0/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.0/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.0/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.0/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.0/plugins/surf": "/docs/plugins/surf", - "/docs/4.0/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.0/plugins/transit": "/docs/plugins/transit", - "/docs/4.0/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.0/plugins/weather": "/docs/plugins/weather", - "/docs/4.0/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.0/reference/accessibility": "/docs/reference/accessibility", "/docs/4.0/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.0/reference/character-codes": "/docs/reference/character-codes", @@ -1349,6 +1111,7 @@ "/docs/4.1/deployment/production": "/docs/deployment/production", "/docs/4.1/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.1/development/contributing": "/docs/development/contributing", + "/docs/4.1/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.1/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.1/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.1/development/testing": "/docs/development/testing", @@ -1362,34 +1125,6 @@ "/docs/4.1/features/transitions": "/docs/features/transitions", "/docs/4.1/features/updating": "/docs/features/updating", "/docs/4.1/intro": "/docs/intro", - "/docs/4.1/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.1/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.1/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.1/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.1/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.1/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.1/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.1/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.1/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.1/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.1/plugins/muni": "/docs/plugins/muni", - "/docs/4.1/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.1/plugins/overview": "/docs/plugins/overview", - "/docs/4.1/plugins/plex": "/docs/plugins/plex", - "/docs/4.1/plugins/random": "/docs/plugins/random", - "/docs/4.1/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.1/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.1/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.1/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.1/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.1/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.1/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.1/plugins/surf": "/docs/plugins/surf", - "/docs/4.1/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.1/plugins/transit": "/docs/plugins/transit", - "/docs/4.1/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.1/plugins/weather": "/docs/plugins/weather", - "/docs/4.1/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.1/reference/accessibility": "/docs/reference/accessibility", "/docs/4.1/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.1/reference/character-codes": "/docs/reference/character-codes", @@ -1418,6 +1153,7 @@ "/docs/4.2/deployment/production": "/docs/deployment/production", "/docs/4.2/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.2/development/contributing": "/docs/development/contributing", + "/docs/4.2/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.2/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.2/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.2/development/testing": "/docs/development/testing", @@ -1431,34 +1167,6 @@ "/docs/4.2/features/transitions": "/docs/features/transitions", "/docs/4.2/features/updating": "/docs/features/updating", "/docs/4.2/intro": "/docs/intro", - "/docs/4.2/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.2/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.2/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.2/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.2/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.2/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.2/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.2/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.2/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.2/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.2/plugins/muni": "/docs/plugins/muni", - "/docs/4.2/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.2/plugins/overview": "/docs/plugins/overview", - "/docs/4.2/plugins/plex": "/docs/plugins/plex", - "/docs/4.2/plugins/random": "/docs/plugins/random", - "/docs/4.2/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.2/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.2/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.2/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.2/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.2/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.2/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.2/plugins/surf": "/docs/plugins/surf", - "/docs/4.2/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.2/plugins/transit": "/docs/plugins/transit", - "/docs/4.2/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.2/plugins/weather": "/docs/plugins/weather", - "/docs/4.2/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.2/reference/accessibility": "/docs/reference/accessibility", "/docs/4.2/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.2/reference/character-codes": "/docs/reference/character-codes", @@ -1487,6 +1195,7 @@ "/docs/4.3/deployment/production": "/docs/deployment/production", "/docs/4.3/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.3/development/contributing": "/docs/development/contributing", + "/docs/4.3/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.3/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.3/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.3/development/testing": "/docs/development/testing", @@ -1500,34 +1209,6 @@ "/docs/4.3/features/transitions": "/docs/features/transitions", "/docs/4.3/features/updating": "/docs/features/updating", "/docs/4.3/intro": "/docs/intro", - "/docs/4.3/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.3/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.3/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.3/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.3/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.3/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.3/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.3/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.3/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.3/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.3/plugins/muni": "/docs/plugins/muni", - "/docs/4.3/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.3/plugins/overview": "/docs/plugins/overview", - "/docs/4.3/plugins/plex": "/docs/plugins/plex", - "/docs/4.3/plugins/random": "/docs/plugins/random", - "/docs/4.3/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.3/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.3/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.3/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.3/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.3/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.3/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.3/plugins/surf": "/docs/plugins/surf", - "/docs/4.3/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.3/plugins/transit": "/docs/plugins/transit", - "/docs/4.3/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.3/plugins/weather": "/docs/plugins/weather", - "/docs/4.3/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.3/reference/accessibility": "/docs/reference/accessibility", "/docs/4.3/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.3/reference/character-codes": "/docs/reference/character-codes", @@ -1556,6 +1237,7 @@ "/docs/4.4/deployment/production": "/docs/deployment/production", "/docs/4.4/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.4/development/contributing": "/docs/development/contributing", + "/docs/4.4/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.4/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.4/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.4/development/testing": "/docs/development/testing", @@ -1569,34 +1251,6 @@ "/docs/4.4/features/transitions": "/docs/features/transitions", "/docs/4.4/features/updating": "/docs/features/updating", "/docs/4.4/intro": "/docs/intro", - "/docs/4.4/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.4/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.4/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.4/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.4/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.4/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.4/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.4/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.4/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.4/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.4/plugins/muni": "/docs/plugins/muni", - "/docs/4.4/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.4/plugins/overview": "/docs/plugins/overview", - "/docs/4.4/plugins/plex": "/docs/plugins/plex", - "/docs/4.4/plugins/random": "/docs/plugins/random", - "/docs/4.4/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.4/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.4/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.4/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.4/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.4/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.4/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.4/plugins/surf": "/docs/plugins/surf", - "/docs/4.4/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.4/plugins/transit": "/docs/plugins/transit", - "/docs/4.4/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.4/plugins/weather": "/docs/plugins/weather", - "/docs/4.4/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.4/reference/accessibility": "/docs/reference/accessibility", "/docs/4.4/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.4/reference/character-codes": "/docs/reference/character-codes", @@ -1625,6 +1279,7 @@ "/docs/4.5/deployment/production": "/docs/deployment/production", "/docs/4.5/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.5/development/contributing": "/docs/development/contributing", + "/docs/4.5/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.5/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.5/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.5/development/testing": "/docs/development/testing", @@ -1638,34 +1293,6 @@ "/docs/4.5/features/transitions": "/docs/features/transitions", "/docs/4.5/features/updating": "/docs/features/updating", "/docs/4.5/intro": "/docs/intro", - "/docs/4.5/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.5/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.5/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.5/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.5/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.5/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.5/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.5/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.5/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.5/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.5/plugins/muni": "/docs/plugins/muni", - "/docs/4.5/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.5/plugins/overview": "/docs/plugins/overview", - "/docs/4.5/plugins/plex": "/docs/plugins/plex", - "/docs/4.5/plugins/random": "/docs/plugins/random", - "/docs/4.5/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.5/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.5/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.5/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.5/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.5/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.5/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.5/plugins/surf": "/docs/plugins/surf", - "/docs/4.5/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.5/plugins/transit": "/docs/plugins/transit", - "/docs/4.5/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.5/plugins/weather": "/docs/plugins/weather", - "/docs/4.5/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.5/reference/accessibility": "/docs/reference/accessibility", "/docs/4.5/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.5/reference/character-codes": "/docs/reference/character-codes", @@ -1694,6 +1321,7 @@ "/docs/4.6/deployment/production": "/docs/deployment/production", "/docs/4.6/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.6/development/contributing": "/docs/development/contributing", + "/docs/4.6/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.6/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.6/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.6/development/testing": "/docs/development/testing", @@ -1707,34 +1335,6 @@ "/docs/4.6/features/transitions": "/docs/features/transitions", "/docs/4.6/features/updating": "/docs/features/updating", "/docs/4.6/intro": "/docs/intro", - "/docs/4.6/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.6/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.6/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.6/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.6/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.6/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.6/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.6/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.6/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.6/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.6/plugins/muni": "/docs/plugins/muni", - "/docs/4.6/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.6/plugins/overview": "/docs/plugins/overview", - "/docs/4.6/plugins/plex": "/docs/plugins/plex", - "/docs/4.6/plugins/random": "/docs/plugins/random", - "/docs/4.6/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.6/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.6/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.6/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.6/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.6/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.6/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.6/plugins/surf": "/docs/plugins/surf", - "/docs/4.6/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.6/plugins/transit": "/docs/plugins/transit", - "/docs/4.6/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.6/plugins/weather": "/docs/plugins/weather", - "/docs/4.6/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.6/reference/accessibility": "/docs/reference/accessibility", "/docs/4.6/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.6/reference/character-codes": "/docs/reference/character-codes", @@ -1763,6 +1363,7 @@ "/docs/4.7/deployment/production": "/docs/deployment/production", "/docs/4.7/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/4.7/development/contributing": "/docs/development/contributing", + "/docs/4.7/development/plugin-ai": "/docs/development/plugin-ai", "/docs/4.7/development/plugin-guide": "/docs/development/plugin-guide", "/docs/4.7/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/4.7/development/testing": "/docs/development/testing", @@ -1776,34 +1377,6 @@ "/docs/4.7/features/transitions": "/docs/features/transitions", "/docs/4.7/features/updating": "/docs/features/updating", "/docs/4.7/intro": "/docs/intro", - "/docs/4.7/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/4.7/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/4.7/plugins/configuration": "/docs/plugins/configuration", - "/docs/4.7/plugins/countdown": "/docs/plugins/countdown", - "/docs/4.7/plugins/date-time": "/docs/plugins/date-time", - "/docs/4.7/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/4.7/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/4.7/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/4.7/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/4.7/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/4.7/plugins/muni": "/docs/plugins/muni", - "/docs/4.7/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/4.7/plugins/overview": "/docs/plugins/overview", - "/docs/4.7/plugins/plex": "/docs/plugins/plex", - "/docs/4.7/plugins/random": "/docs/plugins/random", - "/docs/4.7/plugins/shopify": "/docs/plugins/shopify", - "/docs/4.7/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/4.7/plugins/spotify": "/docs/plugins/spotify", - "/docs/4.7/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/4.7/plugins/stardate": "/docs/plugins/stardate", - "/docs/4.7/plugins/stocks": "/docs/plugins/stocks", - "/docs/4.7/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/4.7/plugins/surf": "/docs/plugins/surf", - "/docs/4.7/plugins/traffic": "/docs/plugins/traffic", - "/docs/4.7/plugins/transit": "/docs/plugins/transit", - "/docs/4.7/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/4.7/plugins/weather": "/docs/plugins/weather", - "/docs/4.7/plugins/wsdot": "/docs/plugins/wsdot", "/docs/4.7/reference/accessibility": "/docs/reference/accessibility", "/docs/4.7/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/4.7/reference/character-codes": "/docs/reference/character-codes", @@ -1832,6 +1405,7 @@ "/docs/5.0/deployment/production": "/docs/deployment/production", "/docs/5.0/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.0/development/contributing": "/docs/development/contributing", + "/docs/5.0/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.0/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.0/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.0/development/testing": "/docs/development/testing", @@ -1845,34 +1419,6 @@ "/docs/5.0/features/transitions": "/docs/features/transitions", "/docs/5.0/features/updating": "/docs/features/updating", "/docs/5.0/intro": "/docs/intro", - "/docs/5.0/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.0/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.0/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.0/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.0/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.0/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.0/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.0/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.0/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.0/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.0/plugins/muni": "/docs/plugins/muni", - "/docs/5.0/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.0/plugins/overview": "/docs/plugins/overview", - "/docs/5.0/plugins/plex": "/docs/plugins/plex", - "/docs/5.0/plugins/random": "/docs/plugins/random", - "/docs/5.0/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.0/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.0/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.0/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.0/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.0/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.0/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.0/plugins/surf": "/docs/plugins/surf", - "/docs/5.0/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.0/plugins/transit": "/docs/plugins/transit", - "/docs/5.0/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.0/plugins/weather": "/docs/plugins/weather", - "/docs/5.0/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.0/reference/accessibility": "/docs/reference/accessibility", "/docs/5.0/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.0/reference/character-codes": "/docs/reference/character-codes", @@ -1901,6 +1447,7 @@ "/docs/5.1/deployment/production": "/docs/deployment/production", "/docs/5.1/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.1/development/contributing": "/docs/development/contributing", + "/docs/5.1/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.1/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.1/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.1/development/testing": "/docs/development/testing", @@ -1914,34 +1461,6 @@ "/docs/5.1/features/transitions": "/docs/features/transitions", "/docs/5.1/features/updating": "/docs/features/updating", "/docs/5.1/intro": "/docs/intro", - "/docs/5.1/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.1/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.1/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.1/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.1/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.1/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.1/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.1/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.1/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.1/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.1/plugins/muni": "/docs/plugins/muni", - "/docs/5.1/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.1/plugins/overview": "/docs/plugins/overview", - "/docs/5.1/plugins/plex": "/docs/plugins/plex", - "/docs/5.1/plugins/random": "/docs/plugins/random", - "/docs/5.1/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.1/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.1/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.1/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.1/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.1/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.1/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.1/plugins/surf": "/docs/plugins/surf", - "/docs/5.1/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.1/plugins/transit": "/docs/plugins/transit", - "/docs/5.1/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.1/plugins/weather": "/docs/plugins/weather", - "/docs/5.1/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.1/reference/accessibility": "/docs/reference/accessibility", "/docs/5.1/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.1/reference/character-codes": "/docs/reference/character-codes", @@ -1970,6 +1489,7 @@ "/docs/5.10/deployment/production": "/docs/deployment/production", "/docs/5.10/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.10/development/contributing": "/docs/development/contributing", + "/docs/5.10/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.10/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.10/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.10/development/testing": "/docs/development/testing", @@ -1983,34 +1503,6 @@ "/docs/5.10/features/transitions": "/docs/features/transitions", "/docs/5.10/features/updating": "/docs/features/updating", "/docs/5.10/intro": "/docs/intro", - "/docs/5.10/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.10/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.10/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.10/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.10/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.10/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.10/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.10/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.10/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.10/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.10/plugins/muni": "/docs/plugins/muni", - "/docs/5.10/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.10/plugins/overview": "/docs/plugins/overview", - "/docs/5.10/plugins/plex": "/docs/plugins/plex", - "/docs/5.10/plugins/random": "/docs/plugins/random", - "/docs/5.10/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.10/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.10/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.10/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.10/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.10/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.10/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.10/plugins/surf": "/docs/plugins/surf", - "/docs/5.10/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.10/plugins/transit": "/docs/plugins/transit", - "/docs/5.10/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.10/plugins/weather": "/docs/plugins/weather", - "/docs/5.10/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.10/reference/accessibility": "/docs/reference/accessibility", "/docs/5.10/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.10/reference/character-codes": "/docs/reference/character-codes", @@ -2039,6 +1531,7 @@ "/docs/5.11/deployment/production": "/docs/deployment/production", "/docs/5.11/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.11/development/contributing": "/docs/development/contributing", + "/docs/5.11/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.11/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.11/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.11/development/testing": "/docs/development/testing", @@ -2052,34 +1545,6 @@ "/docs/5.11/features/transitions": "/docs/features/transitions", "/docs/5.11/features/updating": "/docs/features/updating", "/docs/5.11/intro": "/docs/intro", - "/docs/5.11/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.11/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.11/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.11/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.11/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.11/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.11/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.11/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.11/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.11/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.11/plugins/muni": "/docs/plugins/muni", - "/docs/5.11/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.11/plugins/overview": "/docs/plugins/overview", - "/docs/5.11/plugins/plex": "/docs/plugins/plex", - "/docs/5.11/plugins/random": "/docs/plugins/random", - "/docs/5.11/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.11/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.11/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.11/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.11/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.11/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.11/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.11/plugins/surf": "/docs/plugins/surf", - "/docs/5.11/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.11/plugins/transit": "/docs/plugins/transit", - "/docs/5.11/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.11/plugins/weather": "/docs/plugins/weather", - "/docs/5.11/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.11/reference/accessibility": "/docs/reference/accessibility", "/docs/5.11/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.11/reference/character-codes": "/docs/reference/character-codes", @@ -2108,6 +1573,7 @@ "/docs/5.12/deployment/production": "/docs/deployment/production", "/docs/5.12/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.12/development/contributing": "/docs/development/contributing", + "/docs/5.12/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.12/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.12/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.12/development/testing": "/docs/development/testing", @@ -2121,34 +1587,6 @@ "/docs/5.12/features/transitions": "/docs/features/transitions", "/docs/5.12/features/updating": "/docs/features/updating", "/docs/5.12/intro": "/docs/intro", - "/docs/5.12/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.12/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.12/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.12/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.12/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.12/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.12/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.12/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.12/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.12/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.12/plugins/muni": "/docs/plugins/muni", - "/docs/5.12/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.12/plugins/overview": "/docs/plugins/overview", - "/docs/5.12/plugins/plex": "/docs/plugins/plex", - "/docs/5.12/plugins/random": "/docs/plugins/random", - "/docs/5.12/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.12/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.12/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.12/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.12/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.12/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.12/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.12/plugins/surf": "/docs/plugins/surf", - "/docs/5.12/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.12/plugins/transit": "/docs/plugins/transit", - "/docs/5.12/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.12/plugins/weather": "/docs/plugins/weather", - "/docs/5.12/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.12/reference/accessibility": "/docs/reference/accessibility", "/docs/5.12/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.12/reference/character-codes": "/docs/reference/character-codes", @@ -2177,6 +1615,7 @@ "/docs/5.13/deployment/production": "/docs/deployment/production", "/docs/5.13/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.13/development/contributing": "/docs/development/contributing", + "/docs/5.13/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.13/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.13/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.13/development/testing": "/docs/development/testing", @@ -2190,34 +1629,6 @@ "/docs/5.13/features/transitions": "/docs/features/transitions", "/docs/5.13/features/updating": "/docs/features/updating", "/docs/5.13/intro": "/docs/intro", - "/docs/5.13/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.13/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.13/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.13/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.13/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.13/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.13/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.13/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.13/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.13/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.13/plugins/muni": "/docs/plugins/muni", - "/docs/5.13/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.13/plugins/overview": "/docs/plugins/overview", - "/docs/5.13/plugins/plex": "/docs/plugins/plex", - "/docs/5.13/plugins/random": "/docs/plugins/random", - "/docs/5.13/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.13/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.13/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.13/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.13/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.13/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.13/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.13/plugins/surf": "/docs/plugins/surf", - "/docs/5.13/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.13/plugins/transit": "/docs/plugins/transit", - "/docs/5.13/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.13/plugins/weather": "/docs/plugins/weather", - "/docs/5.13/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.13/reference/accessibility": "/docs/reference/accessibility", "/docs/5.13/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.13/reference/character-codes": "/docs/reference/character-codes", @@ -2246,6 +1657,7 @@ "/docs/5.14/deployment/production": "/docs/deployment/production", "/docs/5.14/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.14/development/contributing": "/docs/development/contributing", + "/docs/5.14/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.14/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.14/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.14/development/testing": "/docs/development/testing", @@ -2259,34 +1671,6 @@ "/docs/5.14/features/transitions": "/docs/features/transitions", "/docs/5.14/features/updating": "/docs/features/updating", "/docs/5.14/intro": "/docs/intro", - "/docs/5.14/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.14/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.14/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.14/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.14/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.14/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.14/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.14/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.14/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.14/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.14/plugins/muni": "/docs/plugins/muni", - "/docs/5.14/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.14/plugins/overview": "/docs/plugins/overview", - "/docs/5.14/plugins/plex": "/docs/plugins/plex", - "/docs/5.14/plugins/random": "/docs/plugins/random", - "/docs/5.14/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.14/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.14/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.14/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.14/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.14/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.14/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.14/plugins/surf": "/docs/plugins/surf", - "/docs/5.14/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.14/plugins/transit": "/docs/plugins/transit", - "/docs/5.14/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.14/plugins/weather": "/docs/plugins/weather", - "/docs/5.14/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.14/reference/accessibility": "/docs/reference/accessibility", "/docs/5.14/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.14/reference/character-codes": "/docs/reference/character-codes", @@ -2315,6 +1699,7 @@ "/docs/5.15/deployment/production": "/docs/deployment/production", "/docs/5.15/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.15/development/contributing": "/docs/development/contributing", + "/docs/5.15/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.15/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.15/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.15/development/testing": "/docs/development/testing", @@ -2328,34 +1713,6 @@ "/docs/5.15/features/transitions": "/docs/features/transitions", "/docs/5.15/features/updating": "/docs/features/updating", "/docs/5.15/intro": "/docs/intro", - "/docs/5.15/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.15/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.15/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.15/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.15/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.15/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.15/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.15/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.15/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.15/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.15/plugins/muni": "/docs/plugins/muni", - "/docs/5.15/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.15/plugins/overview": "/docs/plugins/overview", - "/docs/5.15/plugins/plex": "/docs/plugins/plex", - "/docs/5.15/plugins/random": "/docs/plugins/random", - "/docs/5.15/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.15/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.15/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.15/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.15/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.15/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.15/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.15/plugins/surf": "/docs/plugins/surf", - "/docs/5.15/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.15/plugins/transit": "/docs/plugins/transit", - "/docs/5.15/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.15/plugins/weather": "/docs/plugins/weather", - "/docs/5.15/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.15/reference/accessibility": "/docs/reference/accessibility", "/docs/5.15/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.15/reference/character-codes": "/docs/reference/character-codes", @@ -2384,6 +1741,7 @@ "/docs/5.16/deployment/production": "/docs/deployment/production", "/docs/5.16/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.16/development/contributing": "/docs/development/contributing", + "/docs/5.16/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.16/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.16/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.16/development/testing": "/docs/development/testing", @@ -2397,34 +1755,6 @@ "/docs/5.16/features/transitions": "/docs/features/transitions", "/docs/5.16/features/updating": "/docs/features/updating", "/docs/5.16/intro": "/docs/intro", - "/docs/5.16/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.16/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.16/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.16/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.16/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.16/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.16/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.16/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.16/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.16/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.16/plugins/muni": "/docs/plugins/muni", - "/docs/5.16/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.16/plugins/overview": "/docs/plugins/overview", - "/docs/5.16/plugins/plex": "/docs/plugins/plex", - "/docs/5.16/plugins/random": "/docs/plugins/random", - "/docs/5.16/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.16/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.16/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.16/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.16/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.16/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.16/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.16/plugins/surf": "/docs/plugins/surf", - "/docs/5.16/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.16/plugins/transit": "/docs/plugins/transit", - "/docs/5.16/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.16/plugins/weather": "/docs/plugins/weather", - "/docs/5.16/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.16/reference/accessibility": "/docs/reference/accessibility", "/docs/5.16/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.16/reference/character-codes": "/docs/reference/character-codes", @@ -2453,6 +1783,7 @@ "/docs/5.17/deployment/production": "/docs/deployment/production", "/docs/5.17/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.17/development/contributing": "/docs/development/contributing", + "/docs/5.17/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.17/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.17/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.17/development/testing": "/docs/development/testing", @@ -2466,34 +1797,6 @@ "/docs/5.17/features/transitions": "/docs/features/transitions", "/docs/5.17/features/updating": "/docs/features/updating", "/docs/5.17/intro": "/docs/intro", - "/docs/5.17/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.17/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.17/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.17/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.17/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.17/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.17/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.17/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.17/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.17/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.17/plugins/muni": "/docs/plugins/muni", - "/docs/5.17/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.17/plugins/overview": "/docs/plugins/overview", - "/docs/5.17/plugins/plex": "/docs/plugins/plex", - "/docs/5.17/plugins/random": "/docs/plugins/random", - "/docs/5.17/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.17/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.17/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.17/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.17/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.17/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.17/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.17/plugins/surf": "/docs/plugins/surf", - "/docs/5.17/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.17/plugins/transit": "/docs/plugins/transit", - "/docs/5.17/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.17/plugins/weather": "/docs/plugins/weather", - "/docs/5.17/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.17/reference/accessibility": "/docs/reference/accessibility", "/docs/5.17/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.17/reference/character-codes": "/docs/reference/character-codes", @@ -2522,6 +1825,7 @@ "/docs/5.18/deployment/production": "/docs/deployment/production", "/docs/5.18/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.18/development/contributing": "/docs/development/contributing", + "/docs/5.18/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.18/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.18/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.18/development/testing": "/docs/development/testing", @@ -2535,34 +1839,6 @@ "/docs/5.18/features/transitions": "/docs/features/transitions", "/docs/5.18/features/updating": "/docs/features/updating", "/docs/5.18/intro": "/docs/intro", - "/docs/5.18/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.18/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.18/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.18/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.18/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.18/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.18/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.18/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.18/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.18/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.18/plugins/muni": "/docs/plugins/muni", - "/docs/5.18/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.18/plugins/overview": "/docs/plugins/overview", - "/docs/5.18/plugins/plex": "/docs/plugins/plex", - "/docs/5.18/plugins/random": "/docs/plugins/random", - "/docs/5.18/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.18/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.18/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.18/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.18/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.18/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.18/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.18/plugins/surf": "/docs/plugins/surf", - "/docs/5.18/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.18/plugins/transit": "/docs/plugins/transit", - "/docs/5.18/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.18/plugins/weather": "/docs/plugins/weather", - "/docs/5.18/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.18/reference/accessibility": "/docs/reference/accessibility", "/docs/5.18/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.18/reference/character-codes": "/docs/reference/character-codes", @@ -2591,6 +1867,7 @@ "/docs/5.19/deployment/production": "/docs/deployment/production", "/docs/5.19/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.19/development/contributing": "/docs/development/contributing", + "/docs/5.19/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.19/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.19/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.19/development/testing": "/docs/development/testing", @@ -2604,34 +1881,6 @@ "/docs/5.19/features/transitions": "/docs/features/transitions", "/docs/5.19/features/updating": "/docs/features/updating", "/docs/5.19/intro": "/docs/intro", - "/docs/5.19/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.19/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.19/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.19/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.19/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.19/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.19/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.19/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.19/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.19/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.19/plugins/muni": "/docs/plugins/muni", - "/docs/5.19/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.19/plugins/overview": "/docs/plugins/overview", - "/docs/5.19/plugins/plex": "/docs/plugins/plex", - "/docs/5.19/plugins/random": "/docs/plugins/random", - "/docs/5.19/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.19/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.19/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.19/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.19/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.19/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.19/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.19/plugins/surf": "/docs/plugins/surf", - "/docs/5.19/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.19/plugins/transit": "/docs/plugins/transit", - "/docs/5.19/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.19/plugins/weather": "/docs/plugins/weather", - "/docs/5.19/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.19/reference/accessibility": "/docs/reference/accessibility", "/docs/5.19/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.19/reference/character-codes": "/docs/reference/character-codes", @@ -2660,6 +1909,7 @@ "/docs/5.2/deployment/production": "/docs/deployment/production", "/docs/5.2/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.2/development/contributing": "/docs/development/contributing", + "/docs/5.2/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.2/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.2/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.2/development/testing": "/docs/development/testing", @@ -2673,34 +1923,6 @@ "/docs/5.2/features/transitions": "/docs/features/transitions", "/docs/5.2/features/updating": "/docs/features/updating", "/docs/5.2/intro": "/docs/intro", - "/docs/5.2/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.2/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.2/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.2/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.2/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.2/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.2/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.2/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.2/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.2/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.2/plugins/muni": "/docs/plugins/muni", - "/docs/5.2/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.2/plugins/overview": "/docs/plugins/overview", - "/docs/5.2/plugins/plex": "/docs/plugins/plex", - "/docs/5.2/plugins/random": "/docs/plugins/random", - "/docs/5.2/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.2/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.2/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.2/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.2/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.2/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.2/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.2/plugins/surf": "/docs/plugins/surf", - "/docs/5.2/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.2/plugins/transit": "/docs/plugins/transit", - "/docs/5.2/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.2/plugins/weather": "/docs/plugins/weather", - "/docs/5.2/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.2/reference/accessibility": "/docs/reference/accessibility", "/docs/5.2/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.2/reference/character-codes": "/docs/reference/character-codes", @@ -2729,6 +1951,7 @@ "/docs/5.20/deployment/production": "/docs/deployment/production", "/docs/5.20/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.20/development/contributing": "/docs/development/contributing", + "/docs/5.20/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.20/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.20/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.20/development/testing": "/docs/development/testing", @@ -2742,34 +1965,6 @@ "/docs/5.20/features/transitions": "/docs/features/transitions", "/docs/5.20/features/updating": "/docs/features/updating", "/docs/5.20/intro": "/docs/intro", - "/docs/5.20/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.20/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.20/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.20/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.20/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.20/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.20/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.20/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.20/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.20/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.20/plugins/muni": "/docs/plugins/muni", - "/docs/5.20/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.20/plugins/overview": "/docs/plugins/overview", - "/docs/5.20/plugins/plex": "/docs/plugins/plex", - "/docs/5.20/plugins/random": "/docs/plugins/random", - "/docs/5.20/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.20/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.20/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.20/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.20/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.20/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.20/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.20/plugins/surf": "/docs/plugins/surf", - "/docs/5.20/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.20/plugins/transit": "/docs/plugins/transit", - "/docs/5.20/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.20/plugins/weather": "/docs/plugins/weather", - "/docs/5.20/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.20/reference/accessibility": "/docs/reference/accessibility", "/docs/5.20/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.20/reference/character-codes": "/docs/reference/character-codes", @@ -2798,6 +1993,7 @@ "/docs/5.3/deployment/production": "/docs/deployment/production", "/docs/5.3/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.3/development/contributing": "/docs/development/contributing", + "/docs/5.3/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.3/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.3/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.3/development/testing": "/docs/development/testing", @@ -2811,34 +2007,6 @@ "/docs/5.3/features/transitions": "/docs/features/transitions", "/docs/5.3/features/updating": "/docs/features/updating", "/docs/5.3/intro": "/docs/intro", - "/docs/5.3/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.3/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.3/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.3/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.3/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.3/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.3/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.3/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.3/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.3/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.3/plugins/muni": "/docs/plugins/muni", - "/docs/5.3/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.3/plugins/overview": "/docs/plugins/overview", - "/docs/5.3/plugins/plex": "/docs/plugins/plex", - "/docs/5.3/plugins/random": "/docs/plugins/random", - "/docs/5.3/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.3/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.3/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.3/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.3/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.3/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.3/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.3/plugins/surf": "/docs/plugins/surf", - "/docs/5.3/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.3/plugins/transit": "/docs/plugins/transit", - "/docs/5.3/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.3/plugins/weather": "/docs/plugins/weather", - "/docs/5.3/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.3/reference/accessibility": "/docs/reference/accessibility", "/docs/5.3/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.3/reference/character-codes": "/docs/reference/character-codes", @@ -2867,6 +2035,7 @@ "/docs/5.4/deployment/production": "/docs/deployment/production", "/docs/5.4/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.4/development/contributing": "/docs/development/contributing", + "/docs/5.4/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.4/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.4/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.4/development/testing": "/docs/development/testing", @@ -2880,34 +2049,6 @@ "/docs/5.4/features/transitions": "/docs/features/transitions", "/docs/5.4/features/updating": "/docs/features/updating", "/docs/5.4/intro": "/docs/intro", - "/docs/5.4/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.4/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.4/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.4/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.4/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.4/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.4/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.4/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.4/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.4/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.4/plugins/muni": "/docs/plugins/muni", - "/docs/5.4/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.4/plugins/overview": "/docs/plugins/overview", - "/docs/5.4/plugins/plex": "/docs/plugins/plex", - "/docs/5.4/plugins/random": "/docs/plugins/random", - "/docs/5.4/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.4/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.4/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.4/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.4/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.4/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.4/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.4/plugins/surf": "/docs/plugins/surf", - "/docs/5.4/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.4/plugins/transit": "/docs/plugins/transit", - "/docs/5.4/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.4/plugins/weather": "/docs/plugins/weather", - "/docs/5.4/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.4/reference/accessibility": "/docs/reference/accessibility", "/docs/5.4/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.4/reference/character-codes": "/docs/reference/character-codes", @@ -2936,6 +2077,7 @@ "/docs/5.5/deployment/production": "/docs/deployment/production", "/docs/5.5/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.5/development/contributing": "/docs/development/contributing", + "/docs/5.5/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.5/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.5/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.5/development/testing": "/docs/development/testing", @@ -2949,34 +2091,6 @@ "/docs/5.5/features/transitions": "/docs/features/transitions", "/docs/5.5/features/updating": "/docs/features/updating", "/docs/5.5/intro": "/docs/intro", - "/docs/5.5/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.5/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.5/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.5/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.5/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.5/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.5/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.5/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.5/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.5/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.5/plugins/muni": "/docs/plugins/muni", - "/docs/5.5/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.5/plugins/overview": "/docs/plugins/overview", - "/docs/5.5/plugins/plex": "/docs/plugins/plex", - "/docs/5.5/plugins/random": "/docs/plugins/random", - "/docs/5.5/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.5/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.5/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.5/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.5/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.5/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.5/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.5/plugins/surf": "/docs/plugins/surf", - "/docs/5.5/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.5/plugins/transit": "/docs/plugins/transit", - "/docs/5.5/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.5/plugins/weather": "/docs/plugins/weather", - "/docs/5.5/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.5/reference/accessibility": "/docs/reference/accessibility", "/docs/5.5/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.5/reference/character-codes": "/docs/reference/character-codes", @@ -3005,6 +2119,7 @@ "/docs/5.6/deployment/production": "/docs/deployment/production", "/docs/5.6/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.6/development/contributing": "/docs/development/contributing", + "/docs/5.6/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.6/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.6/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.6/development/testing": "/docs/development/testing", @@ -3018,34 +2133,6 @@ "/docs/5.6/features/transitions": "/docs/features/transitions", "/docs/5.6/features/updating": "/docs/features/updating", "/docs/5.6/intro": "/docs/intro", - "/docs/5.6/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.6/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.6/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.6/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.6/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.6/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.6/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.6/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.6/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.6/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.6/plugins/muni": "/docs/plugins/muni", - "/docs/5.6/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.6/plugins/overview": "/docs/plugins/overview", - "/docs/5.6/plugins/plex": "/docs/plugins/plex", - "/docs/5.6/plugins/random": "/docs/plugins/random", - "/docs/5.6/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.6/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.6/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.6/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.6/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.6/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.6/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.6/plugins/surf": "/docs/plugins/surf", - "/docs/5.6/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.6/plugins/transit": "/docs/plugins/transit", - "/docs/5.6/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.6/plugins/weather": "/docs/plugins/weather", - "/docs/5.6/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.6/reference/accessibility": "/docs/reference/accessibility", "/docs/5.6/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.6/reference/character-codes": "/docs/reference/character-codes", @@ -3074,6 +2161,7 @@ "/docs/5.7/deployment/production": "/docs/deployment/production", "/docs/5.7/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.7/development/contributing": "/docs/development/contributing", + "/docs/5.7/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.7/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.7/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.7/development/testing": "/docs/development/testing", @@ -3087,34 +2175,6 @@ "/docs/5.7/features/transitions": "/docs/features/transitions", "/docs/5.7/features/updating": "/docs/features/updating", "/docs/5.7/intro": "/docs/intro", - "/docs/5.7/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.7/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.7/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.7/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.7/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.7/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.7/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.7/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.7/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.7/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.7/plugins/muni": "/docs/plugins/muni", - "/docs/5.7/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.7/plugins/overview": "/docs/plugins/overview", - "/docs/5.7/plugins/plex": "/docs/plugins/plex", - "/docs/5.7/plugins/random": "/docs/plugins/random", - "/docs/5.7/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.7/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.7/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.7/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.7/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.7/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.7/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.7/plugins/surf": "/docs/plugins/surf", - "/docs/5.7/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.7/plugins/transit": "/docs/plugins/transit", - "/docs/5.7/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.7/plugins/weather": "/docs/plugins/weather", - "/docs/5.7/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.7/reference/accessibility": "/docs/reference/accessibility", "/docs/5.7/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.7/reference/character-codes": "/docs/reference/character-codes", @@ -3143,6 +2203,7 @@ "/docs/5.8/deployment/production": "/docs/deployment/production", "/docs/5.8/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/5.8/development/contributing": "/docs/development/contributing", + "/docs/5.8/development/plugin-ai": "/docs/development/plugin-ai", "/docs/5.8/development/plugin-guide": "/docs/development/plugin-guide", "/docs/5.8/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/5.8/development/testing": "/docs/development/testing", @@ -3156,34 +2217,6 @@ "/docs/5.8/features/transitions": "/docs/features/transitions", "/docs/5.8/features/updating": "/docs/features/updating", "/docs/5.8/intro": "/docs/intro", - "/docs/5.8/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/5.8/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/5.8/plugins/configuration": "/docs/plugins/configuration", - "/docs/5.8/plugins/countdown": "/docs/plugins/countdown", - "/docs/5.8/plugins/date-time": "/docs/plugins/date-time", - "/docs/5.8/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/5.8/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/5.8/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/5.8/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/5.8/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/5.8/plugins/muni": "/docs/plugins/muni", - "/docs/5.8/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/5.8/plugins/overview": "/docs/plugins/overview", - "/docs/5.8/plugins/plex": "/docs/plugins/plex", - "/docs/5.8/plugins/random": "/docs/plugins/random", - "/docs/5.8/plugins/shopify": "/docs/plugins/shopify", - "/docs/5.8/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/5.8/plugins/spotify": "/docs/plugins/spotify", - "/docs/5.8/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/5.8/plugins/stardate": "/docs/plugins/stardate", - "/docs/5.8/plugins/stocks": "/docs/plugins/stocks", - "/docs/5.8/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/5.8/plugins/surf": "/docs/plugins/surf", - "/docs/5.8/plugins/traffic": "/docs/plugins/traffic", - "/docs/5.8/plugins/transit": "/docs/plugins/transit", - "/docs/5.8/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/5.8/plugins/weather": "/docs/plugins/weather", - "/docs/5.8/plugins/wsdot": "/docs/plugins/wsdot", "/docs/5.8/reference/accessibility": "/docs/reference/accessibility", "/docs/5.8/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/5.8/reference/character-codes": "/docs/reference/character-codes", @@ -3212,6 +2245,7 @@ "/docs/6.0/deployment/production": "/docs/deployment/production", "/docs/6.0/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.0/development/contributing": "/docs/development/contributing", + "/docs/6.0/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.0/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.0/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.0/development/testing": "/docs/development/testing", @@ -3225,34 +2259,6 @@ "/docs/6.0/features/transitions": "/docs/features/transitions", "/docs/6.0/features/updating": "/docs/features/updating", "/docs/6.0/intro": "/docs/intro", - "/docs/6.0/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.0/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.0/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.0/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.0/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.0/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.0/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.0/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.0/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.0/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.0/plugins/muni": "/docs/plugins/muni", - "/docs/6.0/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.0/plugins/overview": "/docs/plugins/overview", - "/docs/6.0/plugins/plex": "/docs/plugins/plex", - "/docs/6.0/plugins/random": "/docs/plugins/random", - "/docs/6.0/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.0/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.0/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.0/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.0/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.0/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.0/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.0/plugins/surf": "/docs/plugins/surf", - "/docs/6.0/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.0/plugins/transit": "/docs/plugins/transit", - "/docs/6.0/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.0/plugins/weather": "/docs/plugins/weather", - "/docs/6.0/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.0/reference/accessibility": "/docs/reference/accessibility", "/docs/6.0/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.0/reference/character-codes": "/docs/reference/character-codes", @@ -3281,6 +2287,7 @@ "/docs/6.1/deployment/production": "/docs/deployment/production", "/docs/6.1/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.1/development/contributing": "/docs/development/contributing", + "/docs/6.1/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.1/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.1/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.1/development/testing": "/docs/development/testing", @@ -3294,34 +2301,6 @@ "/docs/6.1/features/transitions": "/docs/features/transitions", "/docs/6.1/features/updating": "/docs/features/updating", "/docs/6.1/intro": "/docs/intro", - "/docs/6.1/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.1/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.1/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.1/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.1/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.1/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.1/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.1/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.1/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.1/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.1/plugins/muni": "/docs/plugins/muni", - "/docs/6.1/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.1/plugins/overview": "/docs/plugins/overview", - "/docs/6.1/plugins/plex": "/docs/plugins/plex", - "/docs/6.1/plugins/random": "/docs/plugins/random", - "/docs/6.1/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.1/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.1/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.1/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.1/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.1/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.1/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.1/plugins/surf": "/docs/plugins/surf", - "/docs/6.1/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.1/plugins/transit": "/docs/plugins/transit", - "/docs/6.1/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.1/plugins/weather": "/docs/plugins/weather", - "/docs/6.1/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.1/reference/accessibility": "/docs/reference/accessibility", "/docs/6.1/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.1/reference/character-codes": "/docs/reference/character-codes", @@ -3350,6 +2329,7 @@ "/docs/6.10/deployment/production": "/docs/deployment/production", "/docs/6.10/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.10/development/contributing": "/docs/development/contributing", + "/docs/6.10/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.10/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.10/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.10/development/testing": "/docs/development/testing", @@ -3363,34 +2343,6 @@ "/docs/6.10/features/transitions": "/docs/features/transitions", "/docs/6.10/features/updating": "/docs/features/updating", "/docs/6.10/intro": "/docs/intro", - "/docs/6.10/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.10/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.10/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.10/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.10/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.10/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.10/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.10/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.10/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.10/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.10/plugins/muni": "/docs/plugins/muni", - "/docs/6.10/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.10/plugins/overview": "/docs/plugins/overview", - "/docs/6.10/plugins/plex": "/docs/plugins/plex", - "/docs/6.10/plugins/random": "/docs/plugins/random", - "/docs/6.10/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.10/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.10/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.10/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.10/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.10/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.10/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.10/plugins/surf": "/docs/plugins/surf", - "/docs/6.10/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.10/plugins/transit": "/docs/plugins/transit", - "/docs/6.10/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.10/plugins/weather": "/docs/plugins/weather", - "/docs/6.10/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.10/reference/accessibility": "/docs/reference/accessibility", "/docs/6.10/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.10/reference/character-codes": "/docs/reference/character-codes", @@ -3419,6 +2371,7 @@ "/docs/6.11/deployment/production": "/docs/deployment/production", "/docs/6.11/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.11/development/contributing": "/docs/development/contributing", + "/docs/6.11/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.11/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.11/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.11/development/testing": "/docs/development/testing", @@ -3432,34 +2385,6 @@ "/docs/6.11/features/transitions": "/docs/features/transitions", "/docs/6.11/features/updating": "/docs/features/updating", "/docs/6.11/intro": "/docs/intro", - "/docs/6.11/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.11/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.11/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.11/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.11/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.11/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.11/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.11/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.11/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.11/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.11/plugins/muni": "/docs/plugins/muni", - "/docs/6.11/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.11/plugins/overview": "/docs/plugins/overview", - "/docs/6.11/plugins/plex": "/docs/plugins/plex", - "/docs/6.11/plugins/random": "/docs/plugins/random", - "/docs/6.11/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.11/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.11/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.11/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.11/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.11/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.11/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.11/plugins/surf": "/docs/plugins/surf", - "/docs/6.11/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.11/plugins/transit": "/docs/plugins/transit", - "/docs/6.11/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.11/plugins/weather": "/docs/plugins/weather", - "/docs/6.11/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.11/reference/accessibility": "/docs/reference/accessibility", "/docs/6.11/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.11/reference/character-codes": "/docs/reference/character-codes", @@ -3488,6 +2413,7 @@ "/docs/6.12/deployment/production": "/docs/deployment/production", "/docs/6.12/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.12/development/contributing": "/docs/development/contributing", + "/docs/6.12/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.12/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.12/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.12/development/testing": "/docs/development/testing", @@ -3501,34 +2427,6 @@ "/docs/6.12/features/transitions": "/docs/features/transitions", "/docs/6.12/features/updating": "/docs/features/updating", "/docs/6.12/intro": "/docs/intro", - "/docs/6.12/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.12/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.12/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.12/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.12/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.12/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.12/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.12/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.12/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.12/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.12/plugins/muni": "/docs/plugins/muni", - "/docs/6.12/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.12/plugins/overview": "/docs/plugins/overview", - "/docs/6.12/plugins/plex": "/docs/plugins/plex", - "/docs/6.12/plugins/random": "/docs/plugins/random", - "/docs/6.12/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.12/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.12/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.12/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.12/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.12/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.12/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.12/plugins/surf": "/docs/plugins/surf", - "/docs/6.12/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.12/plugins/transit": "/docs/plugins/transit", - "/docs/6.12/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.12/plugins/weather": "/docs/plugins/weather", - "/docs/6.12/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.12/reference/accessibility": "/docs/reference/accessibility", "/docs/6.12/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.12/reference/character-codes": "/docs/reference/character-codes", @@ -3557,6 +2455,7 @@ "/docs/6.13/deployment/production": "/docs/deployment/production", "/docs/6.13/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.13/development/contributing": "/docs/development/contributing", + "/docs/6.13/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.13/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.13/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.13/development/testing": "/docs/development/testing", @@ -3570,34 +2469,6 @@ "/docs/6.13/features/transitions": "/docs/features/transitions", "/docs/6.13/features/updating": "/docs/features/updating", "/docs/6.13/intro": "/docs/intro", - "/docs/6.13/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.13/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.13/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.13/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.13/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.13/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.13/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.13/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.13/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.13/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.13/plugins/muni": "/docs/plugins/muni", - "/docs/6.13/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.13/plugins/overview": "/docs/plugins/overview", - "/docs/6.13/plugins/plex": "/docs/plugins/plex", - "/docs/6.13/plugins/random": "/docs/plugins/random", - "/docs/6.13/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.13/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.13/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.13/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.13/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.13/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.13/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.13/plugins/surf": "/docs/plugins/surf", - "/docs/6.13/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.13/plugins/transit": "/docs/plugins/transit", - "/docs/6.13/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.13/plugins/weather": "/docs/plugins/weather", - "/docs/6.13/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.13/reference/accessibility": "/docs/reference/accessibility", "/docs/6.13/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.13/reference/character-codes": "/docs/reference/character-codes", @@ -3626,6 +2497,7 @@ "/docs/6.14/deployment/production": "/docs/deployment/production", "/docs/6.14/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.14/development/contributing": "/docs/development/contributing", + "/docs/6.14/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.14/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.14/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.14/development/testing": "/docs/development/testing", @@ -3639,34 +2511,6 @@ "/docs/6.14/features/transitions": "/docs/features/transitions", "/docs/6.14/features/updating": "/docs/features/updating", "/docs/6.14/intro": "/docs/intro", - "/docs/6.14/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.14/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.14/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.14/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.14/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.14/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.14/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.14/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.14/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.14/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.14/plugins/muni": "/docs/plugins/muni", - "/docs/6.14/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.14/plugins/overview": "/docs/plugins/overview", - "/docs/6.14/plugins/plex": "/docs/plugins/plex", - "/docs/6.14/plugins/random": "/docs/plugins/random", - "/docs/6.14/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.14/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.14/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.14/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.14/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.14/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.14/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.14/plugins/surf": "/docs/plugins/surf", - "/docs/6.14/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.14/plugins/transit": "/docs/plugins/transit", - "/docs/6.14/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.14/plugins/weather": "/docs/plugins/weather", - "/docs/6.14/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.14/reference/accessibility": "/docs/reference/accessibility", "/docs/6.14/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.14/reference/character-codes": "/docs/reference/character-codes", @@ -3695,6 +2539,7 @@ "/docs/6.15/deployment/production": "/docs/deployment/production", "/docs/6.15/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.15/development/contributing": "/docs/development/contributing", + "/docs/6.15/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.15/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.15/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.15/development/testing": "/docs/development/testing", @@ -3708,34 +2553,6 @@ "/docs/6.15/features/transitions": "/docs/features/transitions", "/docs/6.15/features/updating": "/docs/features/updating", "/docs/6.15/intro": "/docs/intro", - "/docs/6.15/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.15/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.15/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.15/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.15/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.15/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.15/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.15/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.15/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.15/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.15/plugins/muni": "/docs/plugins/muni", - "/docs/6.15/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.15/plugins/overview": "/docs/plugins/overview", - "/docs/6.15/plugins/plex": "/docs/plugins/plex", - "/docs/6.15/plugins/random": "/docs/plugins/random", - "/docs/6.15/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.15/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.15/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.15/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.15/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.15/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.15/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.15/plugins/surf": "/docs/plugins/surf", - "/docs/6.15/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.15/plugins/transit": "/docs/plugins/transit", - "/docs/6.15/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.15/plugins/weather": "/docs/plugins/weather", - "/docs/6.15/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.15/reference/accessibility": "/docs/reference/accessibility", "/docs/6.15/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.15/reference/character-codes": "/docs/reference/character-codes", @@ -3764,6 +2581,7 @@ "/docs/6.16/deployment/production": "/docs/deployment/production", "/docs/6.16/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.16/development/contributing": "/docs/development/contributing", + "/docs/6.16/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.16/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.16/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.16/development/testing": "/docs/development/testing", @@ -3777,34 +2595,6 @@ "/docs/6.16/features/transitions": "/docs/features/transitions", "/docs/6.16/features/updating": "/docs/features/updating", "/docs/6.16/intro": "/docs/intro", - "/docs/6.16/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.16/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.16/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.16/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.16/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.16/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.16/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.16/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.16/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.16/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.16/plugins/muni": "/docs/plugins/muni", - "/docs/6.16/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.16/plugins/overview": "/docs/plugins/overview", - "/docs/6.16/plugins/plex": "/docs/plugins/plex", - "/docs/6.16/plugins/random": "/docs/plugins/random", - "/docs/6.16/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.16/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.16/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.16/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.16/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.16/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.16/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.16/plugins/surf": "/docs/plugins/surf", - "/docs/6.16/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.16/plugins/transit": "/docs/plugins/transit", - "/docs/6.16/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.16/plugins/weather": "/docs/plugins/weather", - "/docs/6.16/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.16/reference/accessibility": "/docs/reference/accessibility", "/docs/6.16/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.16/reference/character-codes": "/docs/reference/character-codes", @@ -3833,6 +2623,7 @@ "/docs/6.17/deployment/production": "/docs/deployment/production", "/docs/6.17/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.17/development/contributing": "/docs/development/contributing", + "/docs/6.17/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.17/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.17/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.17/development/testing": "/docs/development/testing", @@ -3846,34 +2637,6 @@ "/docs/6.17/features/transitions": "/docs/features/transitions", "/docs/6.17/features/updating": "/docs/features/updating", "/docs/6.17/intro": "/docs/intro", - "/docs/6.17/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.17/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.17/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.17/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.17/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.17/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.17/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.17/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.17/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.17/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.17/plugins/muni": "/docs/plugins/muni", - "/docs/6.17/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.17/plugins/overview": "/docs/plugins/overview", - "/docs/6.17/plugins/plex": "/docs/plugins/plex", - "/docs/6.17/plugins/random": "/docs/plugins/random", - "/docs/6.17/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.17/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.17/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.17/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.17/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.17/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.17/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.17/plugins/surf": "/docs/plugins/surf", - "/docs/6.17/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.17/plugins/transit": "/docs/plugins/transit", - "/docs/6.17/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.17/plugins/weather": "/docs/plugins/weather", - "/docs/6.17/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.17/reference/accessibility": "/docs/reference/accessibility", "/docs/6.17/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.17/reference/character-codes": "/docs/reference/character-codes", @@ -3902,6 +2665,7 @@ "/docs/6.2/deployment/production": "/docs/deployment/production", "/docs/6.2/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.2/development/contributing": "/docs/development/contributing", + "/docs/6.2/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.2/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.2/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.2/development/testing": "/docs/development/testing", @@ -3915,34 +2679,6 @@ "/docs/6.2/features/transitions": "/docs/features/transitions", "/docs/6.2/features/updating": "/docs/features/updating", "/docs/6.2/intro": "/docs/intro", - "/docs/6.2/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.2/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.2/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.2/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.2/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.2/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.2/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.2/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.2/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.2/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.2/plugins/muni": "/docs/plugins/muni", - "/docs/6.2/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.2/plugins/overview": "/docs/plugins/overview", - "/docs/6.2/plugins/plex": "/docs/plugins/plex", - "/docs/6.2/plugins/random": "/docs/plugins/random", - "/docs/6.2/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.2/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.2/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.2/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.2/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.2/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.2/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.2/plugins/surf": "/docs/plugins/surf", - "/docs/6.2/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.2/plugins/transit": "/docs/plugins/transit", - "/docs/6.2/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.2/plugins/weather": "/docs/plugins/weather", - "/docs/6.2/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.2/reference/accessibility": "/docs/reference/accessibility", "/docs/6.2/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.2/reference/character-codes": "/docs/reference/character-codes", @@ -3971,6 +2707,7 @@ "/docs/6.3/deployment/production": "/docs/deployment/production", "/docs/6.3/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.3/development/contributing": "/docs/development/contributing", + "/docs/6.3/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.3/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.3/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.3/development/testing": "/docs/development/testing", @@ -3984,34 +2721,6 @@ "/docs/6.3/features/transitions": "/docs/features/transitions", "/docs/6.3/features/updating": "/docs/features/updating", "/docs/6.3/intro": "/docs/intro", - "/docs/6.3/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.3/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.3/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.3/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.3/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.3/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.3/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.3/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.3/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.3/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.3/plugins/muni": "/docs/plugins/muni", - "/docs/6.3/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.3/plugins/overview": "/docs/plugins/overview", - "/docs/6.3/plugins/plex": "/docs/plugins/plex", - "/docs/6.3/plugins/random": "/docs/plugins/random", - "/docs/6.3/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.3/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.3/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.3/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.3/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.3/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.3/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.3/plugins/surf": "/docs/plugins/surf", - "/docs/6.3/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.3/plugins/transit": "/docs/plugins/transit", - "/docs/6.3/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.3/plugins/weather": "/docs/plugins/weather", - "/docs/6.3/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.3/reference/accessibility": "/docs/reference/accessibility", "/docs/6.3/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.3/reference/character-codes": "/docs/reference/character-codes", @@ -4040,6 +2749,7 @@ "/docs/6.4/deployment/production": "/docs/deployment/production", "/docs/6.4/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.4/development/contributing": "/docs/development/contributing", + "/docs/6.4/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.4/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.4/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.4/development/testing": "/docs/development/testing", @@ -4053,34 +2763,6 @@ "/docs/6.4/features/transitions": "/docs/features/transitions", "/docs/6.4/features/updating": "/docs/features/updating", "/docs/6.4/intro": "/docs/intro", - "/docs/6.4/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.4/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.4/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.4/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.4/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.4/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.4/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.4/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.4/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.4/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.4/plugins/muni": "/docs/plugins/muni", - "/docs/6.4/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.4/plugins/overview": "/docs/plugins/overview", - "/docs/6.4/plugins/plex": "/docs/plugins/plex", - "/docs/6.4/plugins/random": "/docs/plugins/random", - "/docs/6.4/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.4/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.4/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.4/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.4/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.4/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.4/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.4/plugins/surf": "/docs/plugins/surf", - "/docs/6.4/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.4/plugins/transit": "/docs/plugins/transit", - "/docs/6.4/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.4/plugins/weather": "/docs/plugins/weather", - "/docs/6.4/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.4/reference/accessibility": "/docs/reference/accessibility", "/docs/6.4/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.4/reference/character-codes": "/docs/reference/character-codes", @@ -4109,6 +2791,7 @@ "/docs/6.5/deployment/production": "/docs/deployment/production", "/docs/6.5/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.5/development/contributing": "/docs/development/contributing", + "/docs/6.5/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.5/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.5/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.5/development/testing": "/docs/development/testing", @@ -4122,34 +2805,6 @@ "/docs/6.5/features/transitions": "/docs/features/transitions", "/docs/6.5/features/updating": "/docs/features/updating", "/docs/6.5/intro": "/docs/intro", - "/docs/6.5/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.5/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.5/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.5/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.5/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.5/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.5/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.5/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.5/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.5/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.5/plugins/muni": "/docs/plugins/muni", - "/docs/6.5/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.5/plugins/overview": "/docs/plugins/overview", - "/docs/6.5/plugins/plex": "/docs/plugins/plex", - "/docs/6.5/plugins/random": "/docs/plugins/random", - "/docs/6.5/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.5/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.5/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.5/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.5/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.5/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.5/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.5/plugins/surf": "/docs/plugins/surf", - "/docs/6.5/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.5/plugins/transit": "/docs/plugins/transit", - "/docs/6.5/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.5/plugins/weather": "/docs/plugins/weather", - "/docs/6.5/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.5/reference/accessibility": "/docs/reference/accessibility", "/docs/6.5/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.5/reference/character-codes": "/docs/reference/character-codes", @@ -4178,6 +2833,7 @@ "/docs/6.6/deployment/production": "/docs/deployment/production", "/docs/6.6/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.6/development/contributing": "/docs/development/contributing", + "/docs/6.6/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.6/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.6/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.6/development/testing": "/docs/development/testing", @@ -4191,34 +2847,6 @@ "/docs/6.6/features/transitions": "/docs/features/transitions", "/docs/6.6/features/updating": "/docs/features/updating", "/docs/6.6/intro": "/docs/intro", - "/docs/6.6/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.6/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.6/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.6/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.6/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.6/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.6/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.6/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.6/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.6/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.6/plugins/muni": "/docs/plugins/muni", - "/docs/6.6/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.6/plugins/overview": "/docs/plugins/overview", - "/docs/6.6/plugins/plex": "/docs/plugins/plex", - "/docs/6.6/plugins/random": "/docs/plugins/random", - "/docs/6.6/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.6/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.6/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.6/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.6/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.6/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.6/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.6/plugins/surf": "/docs/plugins/surf", - "/docs/6.6/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.6/plugins/transit": "/docs/plugins/transit", - "/docs/6.6/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.6/plugins/weather": "/docs/plugins/weather", - "/docs/6.6/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.6/reference/accessibility": "/docs/reference/accessibility", "/docs/6.6/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.6/reference/character-codes": "/docs/reference/character-codes", @@ -4247,6 +2875,7 @@ "/docs/6.7/deployment/production": "/docs/deployment/production", "/docs/6.7/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.7/development/contributing": "/docs/development/contributing", + "/docs/6.7/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.7/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.7/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.7/development/testing": "/docs/development/testing", @@ -4260,34 +2889,6 @@ "/docs/6.7/features/transitions": "/docs/features/transitions", "/docs/6.7/features/updating": "/docs/features/updating", "/docs/6.7/intro": "/docs/intro", - "/docs/6.7/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.7/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.7/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.7/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.7/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.7/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.7/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.7/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.7/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.7/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.7/plugins/muni": "/docs/plugins/muni", - "/docs/6.7/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.7/plugins/overview": "/docs/plugins/overview", - "/docs/6.7/plugins/plex": "/docs/plugins/plex", - "/docs/6.7/plugins/random": "/docs/plugins/random", - "/docs/6.7/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.7/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.7/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.7/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.7/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.7/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.7/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.7/plugins/surf": "/docs/plugins/surf", - "/docs/6.7/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.7/plugins/transit": "/docs/plugins/transit", - "/docs/6.7/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.7/plugins/weather": "/docs/plugins/weather", - "/docs/6.7/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.7/reference/accessibility": "/docs/reference/accessibility", "/docs/6.7/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.7/reference/character-codes": "/docs/reference/character-codes", @@ -4316,6 +2917,7 @@ "/docs/6.8/deployment/production": "/docs/deployment/production", "/docs/6.8/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.8/development/contributing": "/docs/development/contributing", + "/docs/6.8/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.8/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.8/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.8/development/testing": "/docs/development/testing", @@ -4329,34 +2931,6 @@ "/docs/6.8/features/transitions": "/docs/features/transitions", "/docs/6.8/features/updating": "/docs/features/updating", "/docs/6.8/intro": "/docs/intro", - "/docs/6.8/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.8/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.8/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.8/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.8/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.8/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.8/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.8/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.8/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.8/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.8/plugins/muni": "/docs/plugins/muni", - "/docs/6.8/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.8/plugins/overview": "/docs/plugins/overview", - "/docs/6.8/plugins/plex": "/docs/plugins/plex", - "/docs/6.8/plugins/random": "/docs/plugins/random", - "/docs/6.8/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.8/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.8/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.8/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.8/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.8/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.8/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.8/plugins/surf": "/docs/plugins/surf", - "/docs/6.8/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.8/plugins/transit": "/docs/plugins/transit", - "/docs/6.8/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.8/plugins/weather": "/docs/plugins/weather", - "/docs/6.8/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.8/reference/accessibility": "/docs/reference/accessibility", "/docs/6.8/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.8/reference/character-codes": "/docs/reference/character-codes", @@ -4385,6 +2959,7 @@ "/docs/6.9/deployment/production": "/docs/deployment/production", "/docs/6.9/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/6.9/development/contributing": "/docs/development/contributing", + "/docs/6.9/development/plugin-ai": "/docs/development/plugin-ai", "/docs/6.9/development/plugin-guide": "/docs/development/plugin-guide", "/docs/6.9/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/6.9/development/testing": "/docs/development/testing", @@ -4398,34 +2973,6 @@ "/docs/6.9/features/transitions": "/docs/features/transitions", "/docs/6.9/features/updating": "/docs/features/updating", "/docs/6.9/intro": "/docs/intro", - "/docs/6.9/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/6.9/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/6.9/plugins/configuration": "/docs/plugins/configuration", - "/docs/6.9/plugins/countdown": "/docs/plugins/countdown", - "/docs/6.9/plugins/date-time": "/docs/plugins/date-time", - "/docs/6.9/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/6.9/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/6.9/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/6.9/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/6.9/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/6.9/plugins/muni": "/docs/plugins/muni", - "/docs/6.9/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/6.9/plugins/overview": "/docs/plugins/overview", - "/docs/6.9/plugins/plex": "/docs/plugins/plex", - "/docs/6.9/plugins/random": "/docs/plugins/random", - "/docs/6.9/plugins/shopify": "/docs/plugins/shopify", - "/docs/6.9/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/6.9/plugins/spotify": "/docs/plugins/spotify", - "/docs/6.9/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/6.9/plugins/stardate": "/docs/plugins/stardate", - "/docs/6.9/plugins/stocks": "/docs/plugins/stocks", - "/docs/6.9/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/6.9/plugins/surf": "/docs/plugins/surf", - "/docs/6.9/plugins/traffic": "/docs/plugins/traffic", - "/docs/6.9/plugins/transit": "/docs/plugins/transit", - "/docs/6.9/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/6.9/plugins/weather": "/docs/plugins/weather", - "/docs/6.9/plugins/wsdot": "/docs/plugins/wsdot", "/docs/6.9/reference/accessibility": "/docs/reference/accessibility", "/docs/6.9/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/6.9/reference/character-codes": "/docs/reference/character-codes", @@ -4454,6 +3001,7 @@ "/docs/7.0/deployment/production": "/docs/deployment/production", "/docs/7.0/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/7.0/development/contributing": "/docs/development/contributing", + "/docs/7.0/development/plugin-ai": "/docs/development/plugin-ai", "/docs/7.0/development/plugin-guide": "/docs/development/plugin-guide", "/docs/7.0/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/7.0/development/testing": "/docs/development/testing", @@ -4467,34 +3015,6 @@ "/docs/7.0/features/transitions": "/docs/features/transitions", "/docs/7.0/features/updating": "/docs/features/updating", "/docs/7.0/intro": "/docs/intro", - "/docs/7.0/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/7.0/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/7.0/plugins/configuration": "/docs/plugins/configuration", - "/docs/7.0/plugins/countdown": "/docs/plugins/countdown", - "/docs/7.0/plugins/date-time": "/docs/plugins/date-time", - "/docs/7.0/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/7.0/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/7.0/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/7.0/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/7.0/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/7.0/plugins/muni": "/docs/plugins/muni", - "/docs/7.0/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/7.0/plugins/overview": "/docs/plugins/overview", - "/docs/7.0/plugins/plex": "/docs/plugins/plex", - "/docs/7.0/plugins/random": "/docs/plugins/random", - "/docs/7.0/plugins/shopify": "/docs/plugins/shopify", - "/docs/7.0/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/7.0/plugins/spotify": "/docs/plugins/spotify", - "/docs/7.0/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/7.0/plugins/stardate": "/docs/plugins/stardate", - "/docs/7.0/plugins/stocks": "/docs/plugins/stocks", - "/docs/7.0/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/7.0/plugins/surf": "/docs/plugins/surf", - "/docs/7.0/plugins/traffic": "/docs/plugins/traffic", - "/docs/7.0/plugins/transit": "/docs/plugins/transit", - "/docs/7.0/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/7.0/plugins/weather": "/docs/plugins/weather", - "/docs/7.0/plugins/wsdot": "/docs/plugins/wsdot", "/docs/7.0/reference/accessibility": "/docs/reference/accessibility", "/docs/7.0/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/7.0/reference/character-codes": "/docs/reference/character-codes", @@ -4523,6 +3043,7 @@ "/docs/7.1/deployment/production": "/docs/deployment/production", "/docs/7.1/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/7.1/development/contributing": "/docs/development/contributing", + "/docs/7.1/development/plugin-ai": "/docs/development/plugin-ai", "/docs/7.1/development/plugin-guide": "/docs/development/plugin-guide", "/docs/7.1/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/7.1/development/testing": "/docs/development/testing", @@ -4536,34 +3057,6 @@ "/docs/7.1/features/transitions": "/docs/features/transitions", "/docs/7.1/features/updating": "/docs/features/updating", "/docs/7.1/intro": "/docs/intro", - "/docs/7.1/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/7.1/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/7.1/plugins/configuration": "/docs/plugins/configuration", - "/docs/7.1/plugins/countdown": "/docs/plugins/countdown", - "/docs/7.1/plugins/date-time": "/docs/plugins/date-time", - "/docs/7.1/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/7.1/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/7.1/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/7.1/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/7.1/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/7.1/plugins/muni": "/docs/plugins/muni", - "/docs/7.1/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/7.1/plugins/overview": "/docs/plugins/overview", - "/docs/7.1/plugins/plex": "/docs/plugins/plex", - "/docs/7.1/plugins/random": "/docs/plugins/random", - "/docs/7.1/plugins/shopify": "/docs/plugins/shopify", - "/docs/7.1/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/7.1/plugins/spotify": "/docs/plugins/spotify", - "/docs/7.1/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/7.1/plugins/stardate": "/docs/plugins/stardate", - "/docs/7.1/plugins/stocks": "/docs/plugins/stocks", - "/docs/7.1/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/7.1/plugins/surf": "/docs/plugins/surf", - "/docs/7.1/plugins/traffic": "/docs/plugins/traffic", - "/docs/7.1/plugins/transit": "/docs/plugins/transit", - "/docs/7.1/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/7.1/plugins/weather": "/docs/plugins/weather", - "/docs/7.1/plugins/wsdot": "/docs/plugins/wsdot", "/docs/7.1/reference/accessibility": "/docs/reference/accessibility", "/docs/7.1/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/7.1/reference/character-codes": "/docs/reference/character-codes", @@ -4592,6 +3085,7 @@ "/docs/7.11/deployment/production": "/docs/deployment/production", "/docs/7.11/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/7.11/development/contributing": "/docs/development/contributing", + "/docs/7.11/development/plugin-ai": "/docs/development/plugin-ai", "/docs/7.11/development/plugin-guide": "/docs/development/plugin-guide", "/docs/7.11/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/7.11/development/testing": "/docs/development/testing", @@ -4605,34 +3099,6 @@ "/docs/7.11/features/transitions": "/docs/features/transitions", "/docs/7.11/features/updating": "/docs/features/updating", "/docs/7.11/intro": "/docs/intro", - "/docs/7.11/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/7.11/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/7.11/plugins/configuration": "/docs/plugins/configuration", - "/docs/7.11/plugins/countdown": "/docs/plugins/countdown", - "/docs/7.11/plugins/date-time": "/docs/plugins/date-time", - "/docs/7.11/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/7.11/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/7.11/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/7.11/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/7.11/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/7.11/plugins/muni": "/docs/plugins/muni", - "/docs/7.11/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/7.11/plugins/overview": "/docs/plugins/overview", - "/docs/7.11/plugins/plex": "/docs/plugins/plex", - "/docs/7.11/plugins/random": "/docs/plugins/random", - "/docs/7.11/plugins/shopify": "/docs/plugins/shopify", - "/docs/7.11/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/7.11/plugins/spotify": "/docs/plugins/spotify", - "/docs/7.11/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/7.11/plugins/stardate": "/docs/plugins/stardate", - "/docs/7.11/plugins/stocks": "/docs/plugins/stocks", - "/docs/7.11/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/7.11/plugins/surf": "/docs/plugins/surf", - "/docs/7.11/plugins/traffic": "/docs/plugins/traffic", - "/docs/7.11/plugins/transit": "/docs/plugins/transit", - "/docs/7.11/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/7.11/plugins/weather": "/docs/plugins/weather", - "/docs/7.11/plugins/wsdot": "/docs/plugins/wsdot", "/docs/7.11/reference/accessibility": "/docs/reference/accessibility", "/docs/7.11/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/7.11/reference/character-codes": "/docs/reference/character-codes", @@ -4661,6 +3127,7 @@ "/docs/7.2/deployment/production": "/docs/deployment/production", "/docs/7.2/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/7.2/development/contributing": "/docs/development/contributing", + "/docs/7.2/development/plugin-ai": "/docs/development/plugin-ai", "/docs/7.2/development/plugin-guide": "/docs/development/plugin-guide", "/docs/7.2/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/7.2/development/testing": "/docs/development/testing", @@ -4674,34 +3141,6 @@ "/docs/7.2/features/transitions": "/docs/features/transitions", "/docs/7.2/features/updating": "/docs/features/updating", "/docs/7.2/intro": "/docs/intro", - "/docs/7.2/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/7.2/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/7.2/plugins/configuration": "/docs/plugins/configuration", - "/docs/7.2/plugins/countdown": "/docs/plugins/countdown", - "/docs/7.2/plugins/date-time": "/docs/plugins/date-time", - "/docs/7.2/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/7.2/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/7.2/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/7.2/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/7.2/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/7.2/plugins/muni": "/docs/plugins/muni", - "/docs/7.2/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/7.2/plugins/overview": "/docs/plugins/overview", - "/docs/7.2/plugins/plex": "/docs/plugins/plex", - "/docs/7.2/plugins/random": "/docs/plugins/random", - "/docs/7.2/plugins/shopify": "/docs/plugins/shopify", - "/docs/7.2/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/7.2/plugins/spotify": "/docs/plugins/spotify", - "/docs/7.2/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/7.2/plugins/stardate": "/docs/plugins/stardate", - "/docs/7.2/plugins/stocks": "/docs/plugins/stocks", - "/docs/7.2/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/7.2/plugins/surf": "/docs/plugins/surf", - "/docs/7.2/plugins/traffic": "/docs/plugins/traffic", - "/docs/7.2/plugins/transit": "/docs/plugins/transit", - "/docs/7.2/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/7.2/plugins/weather": "/docs/plugins/weather", - "/docs/7.2/plugins/wsdot": "/docs/plugins/wsdot", "/docs/7.2/reference/accessibility": "/docs/reference/accessibility", "/docs/7.2/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/7.2/reference/character-codes": "/docs/reference/character-codes", @@ -4730,6 +3169,7 @@ "/docs/7.3/deployment/production": "/docs/deployment/production", "/docs/7.3/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/7.3/development/contributing": "/docs/development/contributing", + "/docs/7.3/development/plugin-ai": "/docs/development/plugin-ai", "/docs/7.3/development/plugin-guide": "/docs/development/plugin-guide", "/docs/7.3/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/7.3/development/testing": "/docs/development/testing", @@ -4743,34 +3183,6 @@ "/docs/7.3/features/transitions": "/docs/features/transitions", "/docs/7.3/features/updating": "/docs/features/updating", "/docs/7.3/intro": "/docs/intro", - "/docs/7.3/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/7.3/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/7.3/plugins/configuration": "/docs/plugins/configuration", - "/docs/7.3/plugins/countdown": "/docs/plugins/countdown", - "/docs/7.3/plugins/date-time": "/docs/plugins/date-time", - "/docs/7.3/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/7.3/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/7.3/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/7.3/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/7.3/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/7.3/plugins/muni": "/docs/plugins/muni", - "/docs/7.3/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/7.3/plugins/overview": "/docs/plugins/overview", - "/docs/7.3/plugins/plex": "/docs/plugins/plex", - "/docs/7.3/plugins/random": "/docs/plugins/random", - "/docs/7.3/plugins/shopify": "/docs/plugins/shopify", - "/docs/7.3/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/7.3/plugins/spotify": "/docs/plugins/spotify", - "/docs/7.3/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/7.3/plugins/stardate": "/docs/plugins/stardate", - "/docs/7.3/plugins/stocks": "/docs/plugins/stocks", - "/docs/7.3/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/7.3/plugins/surf": "/docs/plugins/surf", - "/docs/7.3/plugins/traffic": "/docs/plugins/traffic", - "/docs/7.3/plugins/transit": "/docs/plugins/transit", - "/docs/7.3/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/7.3/plugins/weather": "/docs/plugins/weather", - "/docs/7.3/plugins/wsdot": "/docs/plugins/wsdot", "/docs/7.3/reference/accessibility": "/docs/reference/accessibility", "/docs/7.3/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/7.3/reference/character-codes": "/docs/reference/character-codes", @@ -4799,6 +3211,7 @@ "/docs/8.0/deployment/production": "/docs/deployment/production", "/docs/8.0/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.0/development/contributing": "/docs/development/contributing", + "/docs/8.0/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.0/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.0/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.0/development/testing": "/docs/development/testing", @@ -4812,34 +3225,6 @@ "/docs/8.0/features/transitions": "/docs/features/transitions", "/docs/8.0/features/updating": "/docs/features/updating", "/docs/8.0/intro": "/docs/intro", - "/docs/8.0/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.0/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.0/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.0/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.0/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.0/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.0/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.0/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.0/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.0/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.0/plugins/muni": "/docs/plugins/muni", - "/docs/8.0/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.0/plugins/overview": "/docs/plugins/overview", - "/docs/8.0/plugins/plex": "/docs/plugins/plex", - "/docs/8.0/plugins/random": "/docs/plugins/random", - "/docs/8.0/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.0/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.0/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.0/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.0/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.0/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.0/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.0/plugins/surf": "/docs/plugins/surf", - "/docs/8.0/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.0/plugins/transit": "/docs/plugins/transit", - "/docs/8.0/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.0/plugins/weather": "/docs/plugins/weather", - "/docs/8.0/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.0/reference/accessibility": "/docs/reference/accessibility", "/docs/8.0/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.0/reference/character-codes": "/docs/reference/character-codes", @@ -4868,6 +3253,7 @@ "/docs/8.1/deployment/production": "/docs/deployment/production", "/docs/8.1/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.1/development/contributing": "/docs/development/contributing", + "/docs/8.1/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.1/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.1/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.1/development/testing": "/docs/development/testing", @@ -4881,34 +3267,6 @@ "/docs/8.1/features/transitions": "/docs/features/transitions", "/docs/8.1/features/updating": "/docs/features/updating", "/docs/8.1/intro": "/docs/intro", - "/docs/8.1/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.1/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.1/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.1/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.1/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.1/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.1/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.1/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.1/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.1/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.1/plugins/muni": "/docs/plugins/muni", - "/docs/8.1/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.1/plugins/overview": "/docs/plugins/overview", - "/docs/8.1/plugins/plex": "/docs/plugins/plex", - "/docs/8.1/plugins/random": "/docs/plugins/random", - "/docs/8.1/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.1/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.1/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.1/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.1/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.1/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.1/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.1/plugins/surf": "/docs/plugins/surf", - "/docs/8.1/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.1/plugins/transit": "/docs/plugins/transit", - "/docs/8.1/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.1/plugins/weather": "/docs/plugins/weather", - "/docs/8.1/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.1/reference/accessibility": "/docs/reference/accessibility", "/docs/8.1/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.1/reference/character-codes": "/docs/reference/character-codes", @@ -4937,6 +3295,7 @@ "/docs/8.10/deployment/production": "/docs/deployment/production", "/docs/8.10/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.10/development/contributing": "/docs/development/contributing", + "/docs/8.10/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.10/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.10/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.10/development/testing": "/docs/development/testing", @@ -4950,34 +3309,6 @@ "/docs/8.10/features/transitions": "/docs/features/transitions", "/docs/8.10/features/updating": "/docs/features/updating", "/docs/8.10/intro": "/docs/intro", - "/docs/8.10/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.10/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.10/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.10/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.10/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.10/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.10/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.10/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.10/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.10/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.10/plugins/muni": "/docs/plugins/muni", - "/docs/8.10/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.10/plugins/overview": "/docs/plugins/overview", - "/docs/8.10/plugins/plex": "/docs/plugins/plex", - "/docs/8.10/plugins/random": "/docs/plugins/random", - "/docs/8.10/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.10/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.10/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.10/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.10/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.10/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.10/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.10/plugins/surf": "/docs/plugins/surf", - "/docs/8.10/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.10/plugins/transit": "/docs/plugins/transit", - "/docs/8.10/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.10/plugins/weather": "/docs/plugins/weather", - "/docs/8.10/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.10/reference/accessibility": "/docs/reference/accessibility", "/docs/8.10/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.10/reference/character-codes": "/docs/reference/character-codes", @@ -5006,6 +3337,7 @@ "/docs/8.11/deployment/production": "/docs/deployment/production", "/docs/8.11/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.11/development/contributing": "/docs/development/contributing", + "/docs/8.11/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.11/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.11/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.11/development/testing": "/docs/development/testing", @@ -5019,34 +3351,6 @@ "/docs/8.11/features/transitions": "/docs/features/transitions", "/docs/8.11/features/updating": "/docs/features/updating", "/docs/8.11/intro": "/docs/intro", - "/docs/8.11/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.11/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.11/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.11/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.11/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.11/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.11/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.11/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.11/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.11/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.11/plugins/muni": "/docs/plugins/muni", - "/docs/8.11/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.11/plugins/overview": "/docs/plugins/overview", - "/docs/8.11/plugins/plex": "/docs/plugins/plex", - "/docs/8.11/plugins/random": "/docs/plugins/random", - "/docs/8.11/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.11/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.11/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.11/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.11/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.11/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.11/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.11/plugins/surf": "/docs/plugins/surf", - "/docs/8.11/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.11/plugins/transit": "/docs/plugins/transit", - "/docs/8.11/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.11/plugins/weather": "/docs/plugins/weather", - "/docs/8.11/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.11/reference/accessibility": "/docs/reference/accessibility", "/docs/8.11/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.11/reference/character-codes": "/docs/reference/character-codes", @@ -5075,6 +3379,7 @@ "/docs/8.12/deployment/production": "/docs/deployment/production", "/docs/8.12/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.12/development/contributing": "/docs/development/contributing", + "/docs/8.12/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.12/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.12/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.12/development/testing": "/docs/development/testing", @@ -5088,34 +3393,6 @@ "/docs/8.12/features/transitions": "/docs/features/transitions", "/docs/8.12/features/updating": "/docs/features/updating", "/docs/8.12/intro": "/docs/intro", - "/docs/8.12/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.12/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.12/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.12/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.12/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.12/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.12/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.12/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.12/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.12/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.12/plugins/muni": "/docs/plugins/muni", - "/docs/8.12/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.12/plugins/overview": "/docs/plugins/overview", - "/docs/8.12/plugins/plex": "/docs/plugins/plex", - "/docs/8.12/plugins/random": "/docs/plugins/random", - "/docs/8.12/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.12/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.12/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.12/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.12/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.12/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.12/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.12/plugins/surf": "/docs/plugins/surf", - "/docs/8.12/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.12/plugins/transit": "/docs/plugins/transit", - "/docs/8.12/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.12/plugins/weather": "/docs/plugins/weather", - "/docs/8.12/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.12/reference/accessibility": "/docs/reference/accessibility", "/docs/8.12/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.12/reference/character-codes": "/docs/reference/character-codes", @@ -5144,6 +3421,7 @@ "/docs/8.13/deployment/production": "/docs/deployment/production", "/docs/8.13/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.13/development/contributing": "/docs/development/contributing", + "/docs/8.13/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.13/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.13/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.13/development/testing": "/docs/development/testing", @@ -5157,34 +3435,6 @@ "/docs/8.13/features/transitions": "/docs/features/transitions", "/docs/8.13/features/updating": "/docs/features/updating", "/docs/8.13/intro": "/docs/intro", - "/docs/8.13/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.13/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.13/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.13/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.13/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.13/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.13/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.13/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.13/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.13/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.13/plugins/muni": "/docs/plugins/muni", - "/docs/8.13/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.13/plugins/overview": "/docs/plugins/overview", - "/docs/8.13/plugins/plex": "/docs/plugins/plex", - "/docs/8.13/plugins/random": "/docs/plugins/random", - "/docs/8.13/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.13/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.13/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.13/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.13/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.13/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.13/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.13/plugins/surf": "/docs/plugins/surf", - "/docs/8.13/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.13/plugins/transit": "/docs/plugins/transit", - "/docs/8.13/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.13/plugins/weather": "/docs/plugins/weather", - "/docs/8.13/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.13/reference/accessibility": "/docs/reference/accessibility", "/docs/8.13/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.13/reference/character-codes": "/docs/reference/character-codes", @@ -5213,6 +3463,7 @@ "/docs/8.14/deployment/production": "/docs/deployment/production", "/docs/8.14/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.14/development/contributing": "/docs/development/contributing", + "/docs/8.14/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.14/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.14/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.14/development/testing": "/docs/development/testing", @@ -5226,34 +3477,6 @@ "/docs/8.14/features/transitions": "/docs/features/transitions", "/docs/8.14/features/updating": "/docs/features/updating", "/docs/8.14/intro": "/docs/intro", - "/docs/8.14/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.14/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.14/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.14/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.14/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.14/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.14/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.14/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.14/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.14/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.14/plugins/muni": "/docs/plugins/muni", - "/docs/8.14/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.14/plugins/overview": "/docs/plugins/overview", - "/docs/8.14/plugins/plex": "/docs/plugins/plex", - "/docs/8.14/plugins/random": "/docs/plugins/random", - "/docs/8.14/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.14/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.14/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.14/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.14/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.14/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.14/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.14/plugins/surf": "/docs/plugins/surf", - "/docs/8.14/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.14/plugins/transit": "/docs/plugins/transit", - "/docs/8.14/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.14/plugins/weather": "/docs/plugins/weather", - "/docs/8.14/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.14/reference/accessibility": "/docs/reference/accessibility", "/docs/8.14/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.14/reference/character-codes": "/docs/reference/character-codes", @@ -5282,6 +3505,7 @@ "/docs/8.15/deployment/production": "/docs/deployment/production", "/docs/8.15/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.15/development/contributing": "/docs/development/contributing", + "/docs/8.15/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.15/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.15/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.15/development/testing": "/docs/development/testing", @@ -5295,34 +3519,6 @@ "/docs/8.15/features/transitions": "/docs/features/transitions", "/docs/8.15/features/updating": "/docs/features/updating", "/docs/8.15/intro": "/docs/intro", - "/docs/8.15/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.15/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.15/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.15/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.15/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.15/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.15/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.15/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.15/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.15/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.15/plugins/muni": "/docs/plugins/muni", - "/docs/8.15/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.15/plugins/overview": "/docs/plugins/overview", - "/docs/8.15/plugins/plex": "/docs/plugins/plex", - "/docs/8.15/plugins/random": "/docs/plugins/random", - "/docs/8.15/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.15/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.15/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.15/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.15/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.15/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.15/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.15/plugins/surf": "/docs/plugins/surf", - "/docs/8.15/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.15/plugins/transit": "/docs/plugins/transit", - "/docs/8.15/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.15/plugins/weather": "/docs/plugins/weather", - "/docs/8.15/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.15/reference/accessibility": "/docs/reference/accessibility", "/docs/8.15/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.15/reference/character-codes": "/docs/reference/character-codes", @@ -5351,6 +3547,7 @@ "/docs/8.17/deployment/production": "/docs/deployment/production", "/docs/8.17/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.17/development/contributing": "/docs/development/contributing", + "/docs/8.17/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.17/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.17/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.17/development/testing": "/docs/development/testing", @@ -5364,34 +3561,6 @@ "/docs/8.17/features/transitions": "/docs/features/transitions", "/docs/8.17/features/updating": "/docs/features/updating", "/docs/8.17/intro": "/docs/intro", - "/docs/8.17/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.17/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.17/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.17/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.17/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.17/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.17/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.17/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.17/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.17/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.17/plugins/muni": "/docs/plugins/muni", - "/docs/8.17/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.17/plugins/overview": "/docs/plugins/overview", - "/docs/8.17/plugins/plex": "/docs/plugins/plex", - "/docs/8.17/plugins/random": "/docs/plugins/random", - "/docs/8.17/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.17/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.17/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.17/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.17/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.17/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.17/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.17/plugins/surf": "/docs/plugins/surf", - "/docs/8.17/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.17/plugins/transit": "/docs/plugins/transit", - "/docs/8.17/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.17/plugins/weather": "/docs/plugins/weather", - "/docs/8.17/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.17/reference/accessibility": "/docs/reference/accessibility", "/docs/8.17/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.17/reference/character-codes": "/docs/reference/character-codes", @@ -5420,6 +3589,7 @@ "/docs/8.18/deployment/production": "/docs/deployment/production", "/docs/8.18/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.18/development/contributing": "/docs/development/contributing", + "/docs/8.18/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.18/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.18/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.18/development/testing": "/docs/development/testing", @@ -5433,34 +3603,6 @@ "/docs/8.18/features/transitions": "/docs/features/transitions", "/docs/8.18/features/updating": "/docs/features/updating", "/docs/8.18/intro": "/docs/intro", - "/docs/8.18/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.18/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.18/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.18/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.18/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.18/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.18/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.18/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.18/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.18/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.18/plugins/muni": "/docs/plugins/muni", - "/docs/8.18/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.18/plugins/overview": "/docs/plugins/overview", - "/docs/8.18/plugins/plex": "/docs/plugins/plex", - "/docs/8.18/plugins/random": "/docs/plugins/random", - "/docs/8.18/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.18/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.18/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.18/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.18/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.18/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.18/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.18/plugins/surf": "/docs/plugins/surf", - "/docs/8.18/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.18/plugins/transit": "/docs/plugins/transit", - "/docs/8.18/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.18/plugins/weather": "/docs/plugins/weather", - "/docs/8.18/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.18/reference/accessibility": "/docs/reference/accessibility", "/docs/8.18/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.18/reference/character-codes": "/docs/reference/character-codes", @@ -5489,6 +3631,7 @@ "/docs/8.19/deployment/production": "/docs/deployment/production", "/docs/8.19/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.19/development/contributing": "/docs/development/contributing", + "/docs/8.19/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.19/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.19/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.19/development/testing": "/docs/development/testing", @@ -5502,34 +3645,6 @@ "/docs/8.19/features/transitions": "/docs/features/transitions", "/docs/8.19/features/updating": "/docs/features/updating", "/docs/8.19/intro": "/docs/intro", - "/docs/8.19/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.19/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.19/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.19/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.19/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.19/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.19/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.19/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.19/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.19/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.19/plugins/muni": "/docs/plugins/muni", - "/docs/8.19/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.19/plugins/overview": "/docs/plugins/overview", - "/docs/8.19/plugins/plex": "/docs/plugins/plex", - "/docs/8.19/plugins/random": "/docs/plugins/random", - "/docs/8.19/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.19/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.19/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.19/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.19/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.19/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.19/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.19/plugins/surf": "/docs/plugins/surf", - "/docs/8.19/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.19/plugins/transit": "/docs/plugins/transit", - "/docs/8.19/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.19/plugins/weather": "/docs/plugins/weather", - "/docs/8.19/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.19/reference/accessibility": "/docs/reference/accessibility", "/docs/8.19/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.19/reference/character-codes": "/docs/reference/character-codes", @@ -5558,6 +3673,7 @@ "/docs/8.2/deployment/production": "/docs/deployment/production", "/docs/8.2/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.2/development/contributing": "/docs/development/contributing", + "/docs/8.2/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.2/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.2/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.2/development/testing": "/docs/development/testing", @@ -5571,34 +3687,6 @@ "/docs/8.2/features/transitions": "/docs/features/transitions", "/docs/8.2/features/updating": "/docs/features/updating", "/docs/8.2/intro": "/docs/intro", - "/docs/8.2/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.2/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.2/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.2/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.2/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.2/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.2/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.2/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.2/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.2/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.2/plugins/muni": "/docs/plugins/muni", - "/docs/8.2/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.2/plugins/overview": "/docs/plugins/overview", - "/docs/8.2/plugins/plex": "/docs/plugins/plex", - "/docs/8.2/plugins/random": "/docs/plugins/random", - "/docs/8.2/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.2/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.2/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.2/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.2/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.2/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.2/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.2/plugins/surf": "/docs/plugins/surf", - "/docs/8.2/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.2/plugins/transit": "/docs/plugins/transit", - "/docs/8.2/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.2/plugins/weather": "/docs/plugins/weather", - "/docs/8.2/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.2/reference/accessibility": "/docs/reference/accessibility", "/docs/8.2/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.2/reference/character-codes": "/docs/reference/character-codes", @@ -5627,6 +3715,7 @@ "/docs/8.20/deployment/production": "/docs/deployment/production", "/docs/8.20/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.20/development/contributing": "/docs/development/contributing", + "/docs/8.20/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.20/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.20/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.20/development/testing": "/docs/development/testing", @@ -5640,34 +3729,6 @@ "/docs/8.20/features/transitions": "/docs/features/transitions", "/docs/8.20/features/updating": "/docs/features/updating", "/docs/8.20/intro": "/docs/intro", - "/docs/8.20/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.20/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.20/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.20/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.20/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.20/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.20/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.20/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.20/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.20/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.20/plugins/muni": "/docs/plugins/muni", - "/docs/8.20/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.20/plugins/overview": "/docs/plugins/overview", - "/docs/8.20/plugins/plex": "/docs/plugins/plex", - "/docs/8.20/plugins/random": "/docs/plugins/random", - "/docs/8.20/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.20/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.20/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.20/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.20/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.20/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.20/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.20/plugins/surf": "/docs/plugins/surf", - "/docs/8.20/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.20/plugins/transit": "/docs/plugins/transit", - "/docs/8.20/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.20/plugins/weather": "/docs/plugins/weather", - "/docs/8.20/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.20/reference/accessibility": "/docs/reference/accessibility", "/docs/8.20/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.20/reference/character-codes": "/docs/reference/character-codes", @@ -5696,6 +3757,7 @@ "/docs/8.21/deployment/production": "/docs/deployment/production", "/docs/8.21/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.21/development/contributing": "/docs/development/contributing", + "/docs/8.21/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.21/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.21/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.21/development/testing": "/docs/development/testing", @@ -5709,34 +3771,6 @@ "/docs/8.21/features/transitions": "/docs/features/transitions", "/docs/8.21/features/updating": "/docs/features/updating", "/docs/8.21/intro": "/docs/intro", - "/docs/8.21/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.21/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.21/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.21/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.21/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.21/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.21/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.21/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.21/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.21/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.21/plugins/muni": "/docs/plugins/muni", - "/docs/8.21/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.21/plugins/overview": "/docs/plugins/overview", - "/docs/8.21/plugins/plex": "/docs/plugins/plex", - "/docs/8.21/plugins/random": "/docs/plugins/random", - "/docs/8.21/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.21/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.21/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.21/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.21/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.21/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.21/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.21/plugins/surf": "/docs/plugins/surf", - "/docs/8.21/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.21/plugins/transit": "/docs/plugins/transit", - "/docs/8.21/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.21/plugins/weather": "/docs/plugins/weather", - "/docs/8.21/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.21/reference/accessibility": "/docs/reference/accessibility", "/docs/8.21/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.21/reference/character-codes": "/docs/reference/character-codes", @@ -5765,6 +3799,7 @@ "/docs/8.22/deployment/production": "/docs/deployment/production", "/docs/8.22/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.22/development/contributing": "/docs/development/contributing", + "/docs/8.22/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.22/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.22/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.22/development/testing": "/docs/development/testing", @@ -5778,34 +3813,6 @@ "/docs/8.22/features/transitions": "/docs/features/transitions", "/docs/8.22/features/updating": "/docs/features/updating", "/docs/8.22/intro": "/docs/intro", - "/docs/8.22/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.22/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.22/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.22/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.22/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.22/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.22/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.22/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.22/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.22/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.22/plugins/muni": "/docs/plugins/muni", - "/docs/8.22/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.22/plugins/overview": "/docs/plugins/overview", - "/docs/8.22/plugins/plex": "/docs/plugins/plex", - "/docs/8.22/plugins/random": "/docs/plugins/random", - "/docs/8.22/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.22/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.22/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.22/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.22/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.22/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.22/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.22/plugins/surf": "/docs/plugins/surf", - "/docs/8.22/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.22/plugins/transit": "/docs/plugins/transit", - "/docs/8.22/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.22/plugins/weather": "/docs/plugins/weather", - "/docs/8.22/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.22/reference/accessibility": "/docs/reference/accessibility", "/docs/8.22/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.22/reference/character-codes": "/docs/reference/character-codes", @@ -5834,6 +3841,7 @@ "/docs/8.23/deployment/production": "/docs/deployment/production", "/docs/8.23/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.23/development/contributing": "/docs/development/contributing", + "/docs/8.23/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.23/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.23/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.23/development/testing": "/docs/development/testing", @@ -5847,34 +3855,6 @@ "/docs/8.23/features/transitions": "/docs/features/transitions", "/docs/8.23/features/updating": "/docs/features/updating", "/docs/8.23/intro": "/docs/intro", - "/docs/8.23/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.23/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.23/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.23/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.23/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.23/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.23/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.23/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.23/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.23/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.23/plugins/muni": "/docs/plugins/muni", - "/docs/8.23/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.23/plugins/overview": "/docs/plugins/overview", - "/docs/8.23/plugins/plex": "/docs/plugins/plex", - "/docs/8.23/plugins/random": "/docs/plugins/random", - "/docs/8.23/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.23/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.23/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.23/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.23/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.23/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.23/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.23/plugins/surf": "/docs/plugins/surf", - "/docs/8.23/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.23/plugins/transit": "/docs/plugins/transit", - "/docs/8.23/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.23/plugins/weather": "/docs/plugins/weather", - "/docs/8.23/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.23/reference/accessibility": "/docs/reference/accessibility", "/docs/8.23/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.23/reference/character-codes": "/docs/reference/character-codes", @@ -5903,6 +3883,7 @@ "/docs/8.24/deployment/production": "/docs/deployment/production", "/docs/8.24/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.24/development/contributing": "/docs/development/contributing", + "/docs/8.24/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.24/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.24/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.24/development/testing": "/docs/development/testing", @@ -5916,34 +3897,6 @@ "/docs/8.24/features/transitions": "/docs/features/transitions", "/docs/8.24/features/updating": "/docs/features/updating", "/docs/8.24/intro": "/docs/intro", - "/docs/8.24/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.24/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.24/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.24/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.24/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.24/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.24/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.24/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.24/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.24/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.24/plugins/muni": "/docs/plugins/muni", - "/docs/8.24/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.24/plugins/overview": "/docs/plugins/overview", - "/docs/8.24/plugins/plex": "/docs/plugins/plex", - "/docs/8.24/plugins/random": "/docs/plugins/random", - "/docs/8.24/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.24/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.24/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.24/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.24/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.24/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.24/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.24/plugins/surf": "/docs/plugins/surf", - "/docs/8.24/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.24/plugins/transit": "/docs/plugins/transit", - "/docs/8.24/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.24/plugins/weather": "/docs/plugins/weather", - "/docs/8.24/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.24/reference/accessibility": "/docs/reference/accessibility", "/docs/8.24/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.24/reference/character-codes": "/docs/reference/character-codes", @@ -5972,6 +3925,7 @@ "/docs/8.25/deployment/production": "/docs/deployment/production", "/docs/8.25/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.25/development/contributing": "/docs/development/contributing", + "/docs/8.25/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.25/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.25/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.25/development/testing": "/docs/development/testing", @@ -5985,34 +3939,6 @@ "/docs/8.25/features/transitions": "/docs/features/transitions", "/docs/8.25/features/updating": "/docs/features/updating", "/docs/8.25/intro": "/docs/intro", - "/docs/8.25/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.25/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.25/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.25/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.25/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.25/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.25/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.25/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.25/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.25/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.25/plugins/muni": "/docs/plugins/muni", - "/docs/8.25/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.25/plugins/overview": "/docs/plugins/overview", - "/docs/8.25/plugins/plex": "/docs/plugins/plex", - "/docs/8.25/plugins/random": "/docs/plugins/random", - "/docs/8.25/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.25/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.25/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.25/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.25/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.25/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.25/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.25/plugins/surf": "/docs/plugins/surf", - "/docs/8.25/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.25/plugins/transit": "/docs/plugins/transit", - "/docs/8.25/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.25/plugins/weather": "/docs/plugins/weather", - "/docs/8.25/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.25/reference/accessibility": "/docs/reference/accessibility", "/docs/8.25/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.25/reference/character-codes": "/docs/reference/character-codes", @@ -6041,6 +3967,7 @@ "/docs/8.26/deployment/production": "/docs/deployment/production", "/docs/8.26/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.26/development/contributing": "/docs/development/contributing", + "/docs/8.26/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.26/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.26/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.26/development/testing": "/docs/development/testing", @@ -6054,34 +3981,6 @@ "/docs/8.26/features/transitions": "/docs/features/transitions", "/docs/8.26/features/updating": "/docs/features/updating", "/docs/8.26/intro": "/docs/intro", - "/docs/8.26/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.26/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.26/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.26/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.26/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.26/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.26/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.26/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.26/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.26/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.26/plugins/muni": "/docs/plugins/muni", - "/docs/8.26/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.26/plugins/overview": "/docs/plugins/overview", - "/docs/8.26/plugins/plex": "/docs/plugins/plex", - "/docs/8.26/plugins/random": "/docs/plugins/random", - "/docs/8.26/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.26/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.26/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.26/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.26/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.26/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.26/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.26/plugins/surf": "/docs/plugins/surf", - "/docs/8.26/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.26/plugins/transit": "/docs/plugins/transit", - "/docs/8.26/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.26/plugins/weather": "/docs/plugins/weather", - "/docs/8.26/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.26/reference/accessibility": "/docs/reference/accessibility", "/docs/8.26/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.26/reference/character-codes": "/docs/reference/character-codes", @@ -6110,6 +4009,7 @@ "/docs/8.27/deployment/production": "/docs/deployment/production", "/docs/8.27/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.27/development/contributing": "/docs/development/contributing", + "/docs/8.27/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.27/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.27/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.27/development/testing": "/docs/development/testing", @@ -6123,34 +4023,6 @@ "/docs/8.27/features/transitions": "/docs/features/transitions", "/docs/8.27/features/updating": "/docs/features/updating", "/docs/8.27/intro": "/docs/intro", - "/docs/8.27/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.27/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.27/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.27/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.27/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.27/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.27/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.27/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.27/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.27/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.27/plugins/muni": "/docs/plugins/muni", - "/docs/8.27/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.27/plugins/overview": "/docs/plugins/overview", - "/docs/8.27/plugins/plex": "/docs/plugins/plex", - "/docs/8.27/plugins/random": "/docs/plugins/random", - "/docs/8.27/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.27/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.27/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.27/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.27/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.27/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.27/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.27/plugins/surf": "/docs/plugins/surf", - "/docs/8.27/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.27/plugins/transit": "/docs/plugins/transit", - "/docs/8.27/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.27/plugins/weather": "/docs/plugins/weather", - "/docs/8.27/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.27/reference/accessibility": "/docs/reference/accessibility", "/docs/8.27/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.27/reference/character-codes": "/docs/reference/character-codes", @@ -6179,6 +4051,7 @@ "/docs/8.28/deployment/production": "/docs/deployment/production", "/docs/8.28/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.28/development/contributing": "/docs/development/contributing", + "/docs/8.28/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.28/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.28/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.28/development/testing": "/docs/development/testing", @@ -6192,34 +4065,6 @@ "/docs/8.28/features/transitions": "/docs/features/transitions", "/docs/8.28/features/updating": "/docs/features/updating", "/docs/8.28/intro": "/docs/intro", - "/docs/8.28/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.28/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.28/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.28/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.28/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.28/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.28/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.28/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.28/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.28/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.28/plugins/muni": "/docs/plugins/muni", - "/docs/8.28/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.28/plugins/overview": "/docs/plugins/overview", - "/docs/8.28/plugins/plex": "/docs/plugins/plex", - "/docs/8.28/plugins/random": "/docs/plugins/random", - "/docs/8.28/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.28/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.28/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.28/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.28/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.28/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.28/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.28/plugins/surf": "/docs/plugins/surf", - "/docs/8.28/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.28/plugins/transit": "/docs/plugins/transit", - "/docs/8.28/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.28/plugins/weather": "/docs/plugins/weather", - "/docs/8.28/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.28/reference/accessibility": "/docs/reference/accessibility", "/docs/8.28/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.28/reference/character-codes": "/docs/reference/character-codes", @@ -6243,12 +4088,54 @@ "/docs/8.28/setup/v2-migration": "/docs/setup/v2-migration", "/docs/8.28/setup/v3-migration": "/docs/setup/v3-migration", "/docs/8.28/troubleshooting": "/docs/troubleshooting", - "/docs/8.29": "/docs/8.29/intro", + "/docs/8.29": "/docs/intro", + "/docs/8.29/deployment/fiestaupdater": "/docs/deployment/fiestaupdater", + "/docs/8.29/deployment/production": "/docs/deployment/production", + "/docs/8.29/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", + "/docs/8.29/development/contributing": "/docs/development/contributing", + "/docs/8.29/development/plugin-ai": "/docs/development/plugin-ai", + "/docs/8.29/development/plugin-guide": "/docs/development/plugin-guide", + "/docs/8.29/development/plugin-oauth": "/docs/development/plugin-oauth", + "/docs/8.29/development/testing": "/docs/development/testing", + "/docs/8.29/features/connecting-accounts": "/docs/features/connecting-accounts", + "/docs/8.29/features/fiestapanel": "/docs/features/fiestapanel", + "/docs/8.29/features/home-assistant-control": "/docs/features/home-assistant-control", + "/docs/8.29/features/localization": "/docs/features/localization", + "/docs/8.29/features/page-editor": "/docs/features/page-editor", + "/docs/8.29/features/schedule": "/docs/features/schedule", + "/docs/8.29/features/silence-schedule": "/docs/features/silence-schedule", + "/docs/8.29/features/transitions": "/docs/features/transitions", + "/docs/8.29/features/updating": "/docs/features/updating", + "/docs/8.29/intro": "/docs/intro", + "/docs/8.29/reference/accessibility": "/docs/reference/accessibility", + "/docs/8.29/reference/api-endpoints": "/docs/reference/api-endpoints", + "/docs/8.29/reference/character-codes": "/docs/reference/character-codes", + "/docs/8.29/reference/color-guide": "/docs/reference/color-guide", + "/docs/8.29/reference/environment-variables": "/docs/reference/environment-variables", + "/docs/8.29/reference/template-formulas": "/docs/reference/template-formulas", + "/docs/8.29/setup/ai-providers": "/docs/setup/ai-providers", + "/docs/8.29/setup/api-keys": "/docs/setup/api-keys", + "/docs/8.29/setup/authentication": "/docs/setup/authentication", + "/docs/8.29/setup/beginners-guide": "/docs/setup/beginners-guide", + "/docs/8.29/setup/cloud-api": "/docs/setup/cloud-api", + "/docs/8.29/setup/docker-setup": "/docs/setup/docker-setup", + "/docs/8.29/setup/first-10-minutes": "/docs/setup/first-10-minutes", + "/docs/8.29/setup/home-assistant-addon": "/docs/setup/home-assistant-addon", + "/docs/8.29/setup/home-assistant-mqtt": "/docs/setup/home-assistant-mqtt", + "/docs/8.29/setup/local-development": "/docs/setup/local-development", + "/docs/8.29/setup/local-home-assistant": "/docs/setup/local-home-assistant", + "/docs/8.29/setup/note-arrays": "/docs/setup/note-arrays", + "/docs/8.29/setup/quick-start": "/docs/setup/quick-start", + "/docs/8.29/setup/raspberry-pi": "/docs/setup/raspberry-pi", + "/docs/8.29/setup/v2-migration": "/docs/setup/v2-migration", + "/docs/8.29/setup/v3-migration": "/docs/setup/v3-migration", + "/docs/8.29/troubleshooting": "/docs/troubleshooting", "/docs/8.3": "/docs/intro", "/docs/8.3/deployment/fiestaupdater": "/docs/deployment/fiestaupdater", "/docs/8.3/deployment/production": "/docs/deployment/production", "/docs/8.3/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.3/development/contributing": "/docs/development/contributing", + "/docs/8.3/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.3/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.3/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.3/development/testing": "/docs/development/testing", @@ -6262,34 +4149,6 @@ "/docs/8.3/features/transitions": "/docs/features/transitions", "/docs/8.3/features/updating": "/docs/features/updating", "/docs/8.3/intro": "/docs/intro", - "/docs/8.3/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.3/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.3/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.3/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.3/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.3/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.3/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.3/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.3/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.3/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.3/plugins/muni": "/docs/plugins/muni", - "/docs/8.3/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.3/plugins/overview": "/docs/plugins/overview", - "/docs/8.3/plugins/plex": "/docs/plugins/plex", - "/docs/8.3/plugins/random": "/docs/plugins/random", - "/docs/8.3/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.3/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.3/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.3/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.3/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.3/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.3/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.3/plugins/surf": "/docs/plugins/surf", - "/docs/8.3/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.3/plugins/transit": "/docs/plugins/transit", - "/docs/8.3/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.3/plugins/weather": "/docs/plugins/weather", - "/docs/8.3/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.3/reference/accessibility": "/docs/reference/accessibility", "/docs/8.3/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.3/reference/character-codes": "/docs/reference/character-codes", @@ -6327,6 +4186,7 @@ "/docs/8.4/deployment/production": "/docs/deployment/production", "/docs/8.4/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.4/development/contributing": "/docs/development/contributing", + "/docs/8.4/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.4/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.4/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.4/development/testing": "/docs/development/testing", @@ -6340,34 +4200,6 @@ "/docs/8.4/features/transitions": "/docs/features/transitions", "/docs/8.4/features/updating": "/docs/features/updating", "/docs/8.4/intro": "/docs/intro", - "/docs/8.4/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.4/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.4/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.4/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.4/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.4/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.4/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.4/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.4/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.4/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.4/plugins/muni": "/docs/plugins/muni", - "/docs/8.4/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.4/plugins/overview": "/docs/plugins/overview", - "/docs/8.4/plugins/plex": "/docs/plugins/plex", - "/docs/8.4/plugins/random": "/docs/plugins/random", - "/docs/8.4/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.4/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.4/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.4/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.4/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.4/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.4/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.4/plugins/surf": "/docs/plugins/surf", - "/docs/8.4/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.4/plugins/transit": "/docs/plugins/transit", - "/docs/8.4/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.4/plugins/weather": "/docs/plugins/weather", - "/docs/8.4/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.4/reference/accessibility": "/docs/reference/accessibility", "/docs/8.4/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.4/reference/character-codes": "/docs/reference/character-codes", @@ -6396,6 +4228,7 @@ "/docs/8.5/deployment/production": "/docs/deployment/production", "/docs/8.5/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.5/development/contributing": "/docs/development/contributing", + "/docs/8.5/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.5/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.5/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.5/development/testing": "/docs/development/testing", @@ -6409,34 +4242,6 @@ "/docs/8.5/features/transitions": "/docs/features/transitions", "/docs/8.5/features/updating": "/docs/features/updating", "/docs/8.5/intro": "/docs/intro", - "/docs/8.5/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.5/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.5/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.5/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.5/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.5/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.5/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.5/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.5/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.5/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.5/plugins/muni": "/docs/plugins/muni", - "/docs/8.5/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.5/plugins/overview": "/docs/plugins/overview", - "/docs/8.5/plugins/plex": "/docs/plugins/plex", - "/docs/8.5/plugins/random": "/docs/plugins/random", - "/docs/8.5/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.5/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.5/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.5/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.5/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.5/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.5/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.5/plugins/surf": "/docs/plugins/surf", - "/docs/8.5/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.5/plugins/transit": "/docs/plugins/transit", - "/docs/8.5/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.5/plugins/weather": "/docs/plugins/weather", - "/docs/8.5/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.5/reference/accessibility": "/docs/reference/accessibility", "/docs/8.5/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.5/reference/character-codes": "/docs/reference/character-codes", @@ -6465,6 +4270,7 @@ "/docs/8.6/deployment/production": "/docs/deployment/production", "/docs/8.6/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.6/development/contributing": "/docs/development/contributing", + "/docs/8.6/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.6/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.6/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.6/development/testing": "/docs/development/testing", @@ -6478,34 +4284,6 @@ "/docs/8.6/features/transitions": "/docs/features/transitions", "/docs/8.6/features/updating": "/docs/features/updating", "/docs/8.6/intro": "/docs/intro", - "/docs/8.6/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.6/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.6/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.6/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.6/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.6/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.6/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.6/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.6/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.6/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.6/plugins/muni": "/docs/plugins/muni", - "/docs/8.6/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.6/plugins/overview": "/docs/plugins/overview", - "/docs/8.6/plugins/plex": "/docs/plugins/plex", - "/docs/8.6/plugins/random": "/docs/plugins/random", - "/docs/8.6/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.6/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.6/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.6/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.6/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.6/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.6/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.6/plugins/surf": "/docs/plugins/surf", - "/docs/8.6/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.6/plugins/transit": "/docs/plugins/transit", - "/docs/8.6/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.6/plugins/weather": "/docs/plugins/weather", - "/docs/8.6/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.6/reference/accessibility": "/docs/reference/accessibility", "/docs/8.6/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.6/reference/character-codes": "/docs/reference/character-codes", @@ -6534,6 +4312,7 @@ "/docs/8.7/deployment/production": "/docs/deployment/production", "/docs/8.7/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.7/development/contributing": "/docs/development/contributing", + "/docs/8.7/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.7/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.7/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.7/development/testing": "/docs/development/testing", @@ -6547,34 +4326,6 @@ "/docs/8.7/features/transitions": "/docs/features/transitions", "/docs/8.7/features/updating": "/docs/features/updating", "/docs/8.7/intro": "/docs/intro", - "/docs/8.7/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.7/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.7/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.7/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.7/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.7/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.7/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.7/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.7/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.7/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.7/plugins/muni": "/docs/plugins/muni", - "/docs/8.7/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.7/plugins/overview": "/docs/plugins/overview", - "/docs/8.7/plugins/plex": "/docs/plugins/plex", - "/docs/8.7/plugins/random": "/docs/plugins/random", - "/docs/8.7/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.7/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.7/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.7/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.7/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.7/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.7/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.7/plugins/surf": "/docs/plugins/surf", - "/docs/8.7/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.7/plugins/transit": "/docs/plugins/transit", - "/docs/8.7/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.7/plugins/weather": "/docs/plugins/weather", - "/docs/8.7/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.7/reference/accessibility": "/docs/reference/accessibility", "/docs/8.7/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.7/reference/character-codes": "/docs/reference/character-codes", @@ -6603,6 +4354,7 @@ "/docs/8.8/deployment/production": "/docs/deployment/production", "/docs/8.8/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.8/development/contributing": "/docs/development/contributing", + "/docs/8.8/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.8/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.8/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.8/development/testing": "/docs/development/testing", @@ -6616,34 +4368,6 @@ "/docs/8.8/features/transitions": "/docs/features/transitions", "/docs/8.8/features/updating": "/docs/features/updating", "/docs/8.8/intro": "/docs/intro", - "/docs/8.8/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.8/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.8/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.8/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.8/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.8/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.8/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.8/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.8/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.8/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.8/plugins/muni": "/docs/plugins/muni", - "/docs/8.8/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.8/plugins/overview": "/docs/plugins/overview", - "/docs/8.8/plugins/plex": "/docs/plugins/plex", - "/docs/8.8/plugins/random": "/docs/plugins/random", - "/docs/8.8/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.8/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.8/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.8/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.8/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.8/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.8/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.8/plugins/surf": "/docs/plugins/surf", - "/docs/8.8/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.8/plugins/transit": "/docs/plugins/transit", - "/docs/8.8/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.8/plugins/weather": "/docs/plugins/weather", - "/docs/8.8/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.8/reference/accessibility": "/docs/reference/accessibility", "/docs/8.8/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.8/reference/character-codes": "/docs/reference/character-codes", @@ -6672,6 +4396,7 @@ "/docs/8.9/deployment/production": "/docs/deployment/production", "/docs/8.9/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/8.9/development/contributing": "/docs/development/contributing", + "/docs/8.9/development/plugin-ai": "/docs/development/plugin-ai", "/docs/8.9/development/plugin-guide": "/docs/development/plugin-guide", "/docs/8.9/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/8.9/development/testing": "/docs/development/testing", @@ -6685,34 +4410,6 @@ "/docs/8.9/features/transitions": "/docs/features/transitions", "/docs/8.9/features/updating": "/docs/features/updating", "/docs/8.9/intro": "/docs/intro", - "/docs/8.9/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/8.9/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/8.9/plugins/configuration": "/docs/plugins/configuration", - "/docs/8.9/plugins/countdown": "/docs/plugins/countdown", - "/docs/8.9/plugins/date-time": "/docs/plugins/date-time", - "/docs/8.9/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/8.9/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/8.9/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/8.9/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/8.9/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/8.9/plugins/muni": "/docs/plugins/muni", - "/docs/8.9/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/8.9/plugins/overview": "/docs/plugins/overview", - "/docs/8.9/plugins/plex": "/docs/plugins/plex", - "/docs/8.9/plugins/random": "/docs/plugins/random", - "/docs/8.9/plugins/shopify": "/docs/plugins/shopify", - "/docs/8.9/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/8.9/plugins/spotify": "/docs/plugins/spotify", - "/docs/8.9/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/8.9/plugins/stardate": "/docs/plugins/stardate", - "/docs/8.9/plugins/stocks": "/docs/plugins/stocks", - "/docs/8.9/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/8.9/plugins/surf": "/docs/plugins/surf", - "/docs/8.9/plugins/traffic": "/docs/plugins/traffic", - "/docs/8.9/plugins/transit": "/docs/plugins/transit", - "/docs/8.9/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/8.9/plugins/weather": "/docs/plugins/weather", - "/docs/8.9/plugins/wsdot": "/docs/plugins/wsdot", "/docs/8.9/reference/accessibility": "/docs/reference/accessibility", "/docs/8.9/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/8.9/reference/character-codes": "/docs/reference/character-codes", @@ -6736,6 +4433,7 @@ "/docs/8.9/setup/v2-migration": "/docs/setup/v2-migration", "/docs/8.9/setup/v3-migration": "/docs/setup/v3-migration", "/docs/8.9/troubleshooting": "/docs/troubleshooting", + "/docs/9.10": "/docs/9.10/intro", "/docs/9.8": "/docs/9.8/intro", "/docs/category/deployment": "/docs/deployment/production", "/docs/category/development": "/docs/development/contributing", @@ -6748,6 +4446,7 @@ "/docs/latest/deployment/production": "/docs/deployment/production", "/docs/latest/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/latest/development/contributing": "/docs/development/contributing", + "/docs/latest/development/plugin-ai": "/docs/development/plugin-ai", "/docs/latest/development/plugin-guide": "/docs/development/plugin-guide", "/docs/latest/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/latest/development/testing": "/docs/development/testing", @@ -6761,34 +4460,6 @@ "/docs/latest/features/transitions": "/docs/features/transitions", "/docs/latest/features/updating": "/docs/features/updating", "/docs/latest/intro": "/docs/intro", - "/docs/latest/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/latest/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/latest/plugins/configuration": "/docs/plugins/configuration", - "/docs/latest/plugins/countdown": "/docs/plugins/countdown", - "/docs/latest/plugins/date-time": "/docs/plugins/date-time", - "/docs/latest/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/latest/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/latest/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/latest/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/latest/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/latest/plugins/muni": "/docs/plugins/muni", - "/docs/latest/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/latest/plugins/overview": "/docs/plugins/overview", - "/docs/latest/plugins/plex": "/docs/plugins/plex", - "/docs/latest/plugins/random": "/docs/plugins/random", - "/docs/latest/plugins/shopify": "/docs/plugins/shopify", - "/docs/latest/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/latest/plugins/spotify": "/docs/plugins/spotify", - "/docs/latest/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/latest/plugins/stardate": "/docs/plugins/stardate", - "/docs/latest/plugins/stocks": "/docs/plugins/stocks", - "/docs/latest/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/latest/plugins/surf": "/docs/plugins/surf", - "/docs/latest/plugins/traffic": "/docs/plugins/traffic", - "/docs/latest/plugins/transit": "/docs/plugins/transit", - "/docs/latest/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/latest/plugins/weather": "/docs/plugins/weather", - "/docs/latest/plugins/wsdot": "/docs/plugins/wsdot", "/docs/latest/reference/accessibility": "/docs/reference/accessibility", "/docs/latest/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/latest/reference/character-codes": "/docs/reference/character-codes", @@ -6817,6 +4488,7 @@ "/docs/next/deployment/production": "/docs/deployment/production", "/docs/next/deployment/raspberry-pi": "/docs/deployment/raspberry-pi", "/docs/next/development/contributing": "/docs/development/contributing", + "/docs/next/development/plugin-ai": "/docs/development/plugin-ai", "/docs/next/development/plugin-guide": "/docs/development/plugin-guide", "/docs/next/development/plugin-oauth": "/docs/development/plugin-oauth", "/docs/next/development/testing": "/docs/development/testing", @@ -6830,34 +4502,6 @@ "/docs/next/features/transitions": "/docs/features/transitions", "/docs/next/features/updating": "/docs/features/updating", "/docs/next/intro": "/docs/intro", - "/docs/next/plugins/air-fog": "/docs/plugins/air-fog", - "/docs/next/plugins/baywheels": "/docs/plugins/baywheels", - "/docs/next/plugins/configuration": "/docs/plugins/configuration", - "/docs/next/plugins/countdown": "/docs/plugins/countdown", - "/docs/next/plugins/date-time": "/docs/plugins/date-time", - "/docs/next/plugins/disney-parks": "/docs/plugins/disney-parks", - "/docs/next/plugins/entertainment": "/docs/plugins/entertainment", - "/docs/next/plugins/guest-wifi": "/docs/plugins/guest-wifi", - "/docs/next/plugins/home-assistant": "/docs/plugins/home-assistant", - "/docs/next/plugins/last-fm": "/docs/plugins/last-fm", - "/docs/next/plugins/muni": "/docs/plugins/muni", - "/docs/next/plugins/nearby-aircraft": "/docs/plugins/nearby-aircraft", - "/docs/next/plugins/overview": "/docs/plugins/overview", - "/docs/next/plugins/plex": "/docs/plugins/plex", - "/docs/next/plugins/random": "/docs/plugins/random", - "/docs/next/plugins/shopify": "/docs/plugins/shopify", - "/docs/next/plugins/sports-scores": "/docs/plugins/sports-scores", - "/docs/next/plugins/spotify": "/docs/plugins/spotify", - "/docs/next/plugins/star-trek-quotes": "/docs/plugins/star-trek-quotes", - "/docs/next/plugins/stardate": "/docs/plugins/stardate", - "/docs/next/plugins/stocks": "/docs/plugins/stocks", - "/docs/next/plugins/sun-art": "/docs/plugins/sun-art", - "/docs/next/plugins/surf": "/docs/plugins/surf", - "/docs/next/plugins/traffic": "/docs/plugins/traffic", - "/docs/next/plugins/transit": "/docs/plugins/transit", - "/docs/next/plugins/visual-clock": "/docs/plugins/visual-clock", - "/docs/next/plugins/weather": "/docs/plugins/weather", - "/docs/next/plugins/wsdot": "/docs/plugins/wsdot", "/docs/next/reference/accessibility": "/docs/reference/accessibility", "/docs/next/reference/api-endpoints": "/docs/reference/api-endpoints", "/docs/next/reference/character-codes": "/docs/reference/character-codes", @@ -6882,6 +4526,34 @@ "/docs/next/setup/v2-migration": "/docs/setup/v2-migration", "/docs/next/setup/v3-migration": "/docs/setup/v3-migration", "/docs/next/troubleshooting": "/docs/troubleshooting", + "/docs/plugins/air-fog": "/plugins", + "/docs/plugins/baywheels": "/plugins", + "/docs/plugins/configuration": "/plugins", + "/docs/plugins/countdown": "/plugins", + "/docs/plugins/date-time": "/plugins", + "/docs/plugins/disney-parks": "/plugins", + "/docs/plugins/entertainment": "/plugins", + "/docs/plugins/guest-wifi": "/plugins", + "/docs/plugins/home-assistant": "/plugins", + "/docs/plugins/last-fm": "/plugins", + "/docs/plugins/muni": "/plugins", + "/docs/plugins/nearby-aircraft": "/plugins", + "/docs/plugins/overview": "/plugins", + "/docs/plugins/plex": "/plugins", + "/docs/plugins/random": "/plugins", + "/docs/plugins/shopify": "/plugins", + "/docs/plugins/sports-scores": "/plugins", + "/docs/plugins/spotify": "/plugins", + "/docs/plugins/star-trek-quotes": "/plugins", + "/docs/plugins/stardate": "/plugins", + "/docs/plugins/stocks": "/plugins", + "/docs/plugins/sun-art": "/plugins", + "/docs/plugins/surf": "/plugins", + "/docs/plugins/traffic": "/plugins", + "/docs/plugins/transit": "/plugins", + "/docs/plugins/visual-clock": "/plugins", + "/docs/plugins/weather": "/plugins", + "/docs/plugins/wsdot": "/plugins", "/docs/setup/split-flap-display-software": "/docs/intro", "/fiestaui": "/FiestaUI/" }, @@ -6901,6 +4573,7 @@ "/docs/deployment/production", "/docs/deployment/raspberry-pi", "/docs/development/contributing", + "/docs/development/plugin-ai", "/docs/development/plugin-guide", "/docs/development/plugin-oauth", "/docs/development/testing", @@ -6914,34 +4587,6 @@ "/docs/features/transitions", "/docs/features/updating", "/docs/intro", - "/docs/plugins/air-fog", - "/docs/plugins/baywheels", - "/docs/plugins/configuration", - "/docs/plugins/countdown", - "/docs/plugins/date-time", - "/docs/plugins/disney-parks", - "/docs/plugins/entertainment", - "/docs/plugins/guest-wifi", - "/docs/plugins/home-assistant", - "/docs/plugins/last-fm", - "/docs/plugins/muni", - "/docs/plugins/nearby-aircraft", - "/docs/plugins/overview", - "/docs/plugins/plex", - "/docs/plugins/random", - "/docs/plugins/shopify", - "/docs/plugins/sports-scores", - "/docs/plugins/spotify", - "/docs/plugins/star-trek-quotes", - "/docs/plugins/stardate", - "/docs/plugins/stocks", - "/docs/plugins/sun-art", - "/docs/plugins/surf", - "/docs/plugins/traffic", - "/docs/plugins/transit", - "/docs/plugins/visual-clock", - "/docs/plugins/weather", - "/docs/plugins/wsdot", "/docs/reference/accessibility", "/docs/reference/api-endpoints", "/docs/reference/character-codes", @@ -6994,7 +4639,6 @@ "assets/images/stocks-board.png", "assets/images/visual-clock-board.png", "assets/js/0058b4c6.js", - "assets/js/00f74a77.js", "assets/js/01a85c17.js", "assets/js/01d173f4.js", "assets/js/01e7522e.js", @@ -7007,7 +4651,6 @@ "assets/js/057c7ca4.js", "assets/js/05b2fa6e.js", "assets/js/06c3982e.js", - "assets/js/06f69d8f.js", "assets/js/07269b02.js", "assets/js/072eea61.js", "assets/js/07610ff6.js", @@ -7019,11 +4662,10 @@ "assets/js/084738fe.js", "assets/js/0855a97d.js", "assets/js/08698964.js", - "assets/js/087f05b4.js", "assets/js/097a3a79.js", "assets/js/09974f8e.js", "assets/js/09abef1d.js", - "assets/js/0ad74250.js", + "assets/js/0a62b085.js", "assets/js/0ad90326.js", "assets/js/0ad9cf32.js", "assets/js/0b1a18bc.js", @@ -7031,7 +4673,6 @@ "assets/js/0c7cc5cb.js", "assets/js/0cf557fd.js", "assets/js/0d88be51.js", - "assets/js/0dbbca3e.js", "assets/js/0dfc4b6e.js", "assets/js/0e19166d.js", "assets/js/0e8a20df.js", @@ -7040,7 +4681,6 @@ "assets/js/0eea7cb6.js", "assets/js/0ef20aaa.js", "assets/js/0f223293.js", - "assets/js/0f60ad90.js", "assets/js/0fe98bf4.js", "assets/js/1076d442.js", "assets/js/1114c3df.js", @@ -7049,8 +4689,6 @@ "assets/js/11797d5d.js", "assets/js/11884.js", "assets/js/12301f55.js", - "assets/js/128da4c6.js", - "assets/js/12eda238.js", "assets/js/12f5e9a9.js", "assets/js/137cd333.js", "assets/js/13806f55.js", @@ -7060,7 +4698,6 @@ "assets/js/140d5467.js", "assets/js/141d95e3.js", "assets/js/146f7d85.js", - "assets/js/148f7657.js", "assets/js/15517ea5.js", "assets/js/15926.js", "assets/js/15c2c527.js", @@ -7075,9 +4712,9 @@ "assets/js/18996981.js", "assets/js/18d7f3c7.js", "assets/js/18ec9d8f.js", - "assets/js/19405c67.js", "assets/js/1942c1f0.js", "assets/js/199d8663.js", + "assets/js/19e17133.js", "assets/js/1a36d09c.js", "assets/js/1a4e3797.js", "assets/js/1b16b0c0.js", @@ -7085,6 +4722,7 @@ "assets/js/1bea2854.js", "assets/js/1c0f715e.js", "assets/js/1c1984a0.js", + "assets/js/1c460aa5.js", "assets/js/1cf547cd.js", "assets/js/1d56d056.js", "assets/js/1df93b7f.js", @@ -7102,15 +4740,16 @@ "assets/js/21870.js", "assets/js/21bf6080.js", "assets/js/229d86ff.js", - "assets/js/22a962b1.js", "assets/js/23693.js", "assets/js/2374db58.js", "assets/js/239aa3e3.js", "assets/js/240ab723.js", + "assets/js/240cb987.js", "assets/js/24252e95.js", "assets/js/2493ef9a.js", "assets/js/24b90952.js", "assets/js/24baf39b.js", + "assets/js/2506e169.js", "assets/js/25f796b3.js", "assets/js/266ebf3f.js", "assets/js/26dc55ed.js", @@ -7122,9 +4761,7 @@ "assets/js/2824dcc9.js", "assets/js/2837685d.js", "assets/js/285a19e8.js", - "assets/js/28e371c0.js", "assets/js/292a4245.js", - "assets/js/293c27cb.js", "assets/js/29bc9798.js", "assets/js/2a337fd0.js", "assets/js/2a484ba0.js", @@ -7132,13 +4769,13 @@ "assets/js/2a891449.js", "assets/js/2a936f81.js", "assets/js/2ae37587.js", - "assets/js/2b136c56.js", "assets/js/2b1f7a32.js", + "assets/js/2b9057c8.js", "assets/js/2b94b483.js", "assets/js/2bd6018c.js", "assets/js/2be11938.js", "assets/js/2be4bfc3.js", - "assets/js/2ce26910.js", + "assets/js/2c917302.js", "assets/js/2d67265b.js", "assets/js/2d8ffb21.js", "assets/js/2da2e52b.js", @@ -7155,7 +4792,6 @@ "assets/js/2ff147a6.js", "assets/js/2ff244dc.js", "assets/js/3038dca9.js", - "assets/js/3101191d.js", "assets/js/310e6585.js", "assets/js/3114087f.js", "assets/js/3141b647.js", @@ -7171,11 +4807,13 @@ "assets/js/3452ef45.js", "assets/js/3583d1b9.js", "assets/js/35a4de73.js", + "assets/js/35f9746b.js", "assets/js/3606f2e1.js", "assets/js/3617f341.js", "assets/js/36726.js", "assets/js/36994c47.js", "assets/js/36cbe127.js", + "assets/js/3734096a.js", "assets/js/37d6852d.js", "assets/js/37da916e.js", "assets/js/38040bba.js", @@ -7204,7 +4842,6 @@ "assets/js/3c7c1a2b.js", "assets/js/3cada612.js", "assets/js/3d2b5228.js", - "assets/js/3d5f993f.js", "assets/js/3da50f71.js", "assets/js/3db17dff.js", "assets/js/3dbd1d02.js", @@ -7212,21 +4849,20 @@ "assets/js/3e3d1ef2.js", "assets/js/3e500575.js", "assets/js/3e86fdab.js", - "assets/js/3ec69d88.js", "assets/js/3ec83f45.js", "assets/js/3fbec921.js", "assets/js/40092.js", "assets/js/4017938f.js", "assets/js/401b9b2d.js", - "assets/js/4097eb32.js", + "assets/js/40604.js", "assets/js/40ba2e2b.js", "assets/js/4123a59c.js", - "assets/js/420e61cd.js", "assets/js/4235cdf9.js", + "assets/js/4244af45.js", "assets/js/4281a336.js", "assets/js/428f5b8d.js", + "assets/js/43197a5d.js", "assets/js/43698dfc.js", - "assets/js/43e64578.js", "assets/js/43f2ffda.js", "assets/js/4474.js", "assets/js/44929.js", @@ -7257,15 +4893,11 @@ "assets/js/4b5f479a.js", "assets/js/4b61a81d.js", "assets/js/4bc0d599.js", - "assets/js/4c049cd3.js", "assets/js/4c127954.js", "assets/js/4c260043.js", - "assets/js/4cca5eeb.js", - "assets/js/4d80c8eb.js", "assets/js/4db33a9c.js", "assets/js/4e257b39.js", "assets/js/4e63782b.js", - "assets/js/4e9fcb87.js", "assets/js/4ea2c571.js", "assets/js/4eca58ca.js", "assets/js/4ee4c41e.js", @@ -7291,11 +4923,11 @@ "assets/js/5452db38.js", "assets/js/552bb9c2.js", "assets/js/552fa94c.js", - "assets/js/55a4f05f.js", "assets/js/55b60980.js", "assets/js/55da6ebf.js", "assets/js/5675d056.js", "assets/js/56886c9a.js", + "assets/js/56f3cb09.js", "assets/js/575a0ee4.js", "assets/js/5855ff85.js", "assets/js/587c1da5.js", @@ -7305,11 +4937,13 @@ "assets/js/5a210162.js", "assets/js/5a44f784.js", "assets/js/5aa20c47.js", + "assets/js/5aab8c42.js", "assets/js/5afe7e81.js", "assets/js/5b3ad9f6.js", "assets/js/5b8ef940.js", "assets/js/5be816a8.js", "assets/js/5c091ec3.js", + "assets/js/5c1d005b.js", "assets/js/5c1fb736.js", "assets/js/5c591c55.js", "assets/js/5ccf4f65.js", @@ -7333,37 +4967,30 @@ "assets/js/6110616a.js", "assets/js/6210e780.js", "assets/js/621db11d.js", - "assets/js/622f4b8e.js", "assets/js/62cec74e.js", "assets/js/638db5a2.js", - "assets/js/640f9b11.js", "assets/js/643f738e.js", "assets/js/65ca0dff.js", "assets/js/65e2aaff.js", "assets/js/65ed0f8d.js", - "assets/js/660f8ffe.js", "assets/js/662af9cf.js", "assets/js/6639310f.js", "assets/js/665a76ac.js", "assets/js/6692fa84.js", + "assets/js/66b34ff2.js", "assets/js/66cfb387.js", "assets/js/66f8711a.js", "assets/js/674d8e38.js", "assets/js/67930d0b.js", "assets/js/679cca58.js", "assets/js/67bed082.js", - "assets/js/67d0bc26.js", - "assets/js/67d34918.js", "assets/js/6875c492.js", "assets/js/69470fdb.js", "assets/js/6967ec6a.js", - "assets/js/69753.js", "assets/js/69d205d3.js", "assets/js/69dfd298.js", - "assets/js/69e2c1c1.js", "assets/js/69fa3d95.js", "assets/js/6a1a14d4.js", - "assets/js/6a81923f.js", "assets/js/6a8f642c.js", "assets/js/6ad9f109.js", "assets/js/6b0ba70e.js", @@ -7382,7 +5009,6 @@ "assets/js/6de34a46.js", "assets/js/6f4bb194.js", "assets/js/6f66b54a.js", - "assets/js/6fd3562f.js", "assets/js/70072a52.js", "assets/js/70101.js", "assets/js/707c7477.js", @@ -7390,8 +5016,6 @@ "assets/js/71272b2f.js", "assets/js/715240e5.js", "assets/js/7158aef6.js", - "assets/js/715a1de0.js", - "assets/js/7176a71d.js", "assets/js/71d81992.js", "assets/js/7252b3e5.js", "assets/js/72580.js", @@ -7403,30 +5027,29 @@ "assets/js/766f6129.js", "assets/js/76844508.js", "assets/js/7742d184.js", + "assets/js/777139d8.js", "assets/js/77d92566.js", "assets/js/7871605f.js", "assets/js/787198ed.js", "assets/js/7912389e.js", "assets/js/792e0d53.js", + "assets/js/79685a4e.js", "assets/js/799c2888.js", + "assets/js/79b18347.js", + "assets/js/7a3f4f28.js", "assets/js/7a456cb5.js", "assets/js/7a905f6b.js", "assets/js/7b052cb1.js", - "assets/js/7b6d8dd3.js", "assets/js/7b8bbf32.js", - "assets/js/7c918ea0.js", "assets/js/7d1dee15.js", "assets/js/7d4b6da6.js", "assets/js/7d8d1652.js", "assets/js/7dac6902.js", "assets/js/7db3d15c.js", - "assets/js/7dec790f.js", "assets/js/7e6fcb33.js", "assets/js/7ea6dce5.js", "assets/js/7f326d9d.js", "assets/js/7f979206.js", - "assets/js/803e8ce9.js", - "assets/js/8072c625.js", "assets/js/810cd99d.js", "assets/js/814f3328.js", "assets/js/81a9007d.js", @@ -7438,6 +5061,7 @@ "assets/js/82e379f7.js", "assets/js/82f6031c.js", "assets/js/83211aa9.js", + "assets/js/83280.js", "assets/js/83d47b4e.js", "assets/js/83da55d7.js", "assets/js/83fb86eb.js", @@ -7447,7 +5071,6 @@ "assets/js/848f3548.js", "assets/js/84b33585.js", "assets/js/85ce909a.js", - "assets/js/8771011d.js", "assets/js/8780fd15.js", "assets/js/87a4f2d5.js", "assets/js/87b191f0.js", @@ -7455,8 +5078,6 @@ "assets/js/88664aae.js", "assets/js/88c2916b.js", "assets/js/88ef97a9.js", - "assets/js/892c6e33.js", - "assets/js/893f3f52.js", "assets/js/89447.js", "assets/js/89449.js", "assets/js/8954d9b3.js", @@ -7465,13 +5086,14 @@ "assets/js/89ced001.js", "assets/js/8ae4675a.js", "assets/js/8afaedbf.js", - "assets/js/8b874f9e.js", + "assets/js/8b178254.js", "assets/js/8b9d69e6.js", + "assets/js/8bfa13d8.js", "assets/js/8c43ddd6.js", "assets/js/8c8d388c.js", "assets/js/8cb42843.js", "assets/js/8cb74f0e.js", - "assets/js/8d212b70.js", + "assets/js/8d3f194c.js", "assets/js/8d5282ed.js", "assets/js/8dcd0edd.js", "assets/js/8e3a6509.js", @@ -7492,10 +5114,10 @@ "assets/js/92fa47e8.js", "assets/js/9350bd08.js", "assets/js/9387492b.js", + "assets/js/938bfcf3.js", "assets/js/938eaeaf.js", "assets/js/9405ac1e.js", "assets/js/9409e284.js", - "assets/js/9453c572.js", "assets/js/955061fe.js", "assets/js/9585ed54.js", "assets/js/95a1ddfe.js", @@ -7507,6 +5129,8 @@ "assets/js/97f39228.js", "assets/js/988553d2.js", "assets/js/9891ad53.js", + "assets/js/98af0904.js", + "assets/js/992d0a89.js", "assets/js/997c1ba7.js", "assets/js/99d988d2.js", "assets/js/9a452bee.js", @@ -7518,8 +5142,10 @@ "assets/js/9c630765.js", "assets/js/9ce8214b.js", "assets/js/9d25b79a.js", + "assets/js/9d5402ac.js", "assets/js/9d86e7fa.js", "assets/js/9da20568.js", + "assets/js/9e25006c.js", "assets/js/9e4087bc.js", "assets/js/9e5552e1.js", "assets/js/9e5d30bd.js", @@ -7527,21 +5153,19 @@ "assets/js/9f441835.js", "assets/js/9f5a9402.js", "assets/js/9fb22a7a.js", - "assets/js/9fb3b753.js", "assets/js/9fc26e18.js", "assets/js/a061a80f.js", "assets/js/a083a653.js", "assets/js/a0cfd661.js", "assets/js/a111ea7a.js", - "assets/js/a15b563a.js", "assets/js/a181d9d8.js", "assets/js/a19cd987.js", "assets/js/a202d90b.js", "assets/js/a20bfcaf.js", + "assets/js/a282631a.js", "assets/js/a379d66a.js", "assets/js/a3bfd6d6.js", "assets/js/a3d8da39.js", - "assets/js/a4098656.js", "assets/js/a4a99219.js", "assets/js/a547e96d.js", "assets/js/a57f79be.js", @@ -7562,8 +5186,10 @@ "assets/js/aa3d7ffd.js", "assets/js/aa3fc331.js", "assets/js/aa488bf5.js", + "assets/js/aa54983a.js", "assets/js/aa574b6f.js", "assets/js/aa94f71c.js", + "assets/js/aaa8535b.js", "assets/js/ab156724.js", "assets/js/ab5a8c28.js", "assets/js/aba21aa0.js", @@ -7596,10 +5222,7 @@ "assets/js/b3644460.js", "assets/js/b3d5d2c3.js", "assets/js/b40217a0.js", - "assets/js/b45bd922.js", "assets/js/b4d8c825.js", - "assets/js/b4dfb25e.js", - "assets/js/b5466bed.js", "assets/js/b57f5716.js", "assets/js/b592e0ef.js", "assets/js/b5ac768e.js", @@ -7608,6 +5231,7 @@ "assets/js/b621d4db.js", "assets/js/b63be2ac.js", "assets/js/b69961e4.js", + "assets/js/b6d490c6.js", "assets/js/b7741311.js", "assets/js/b7adbfda.js", "assets/js/b802f79a.js", @@ -7645,11 +5269,13 @@ "assets/js/c52e6063.js", "assets/js/c55cbc3d.js", "assets/js/c58a9832.js", + "assets/js/c693fe17.js", "assets/js/c698af99.js", "assets/js/c7898f43.js", "assets/js/c7d28917.js", "assets/js/c7e0eba9.js", "assets/js/c7f8f13a.js", + "assets/js/c8098396.js", "assets/js/c8116ec1.js", "assets/js/c89848e5.js", "assets/js/c89d8a11.js", @@ -7659,10 +5285,10 @@ "assets/js/c9518aa7.js", "assets/js/c9cac9d9.js", "assets/js/ca0f74f4.js", + "assets/js/ca3aaa70.js", "assets/js/ca58b4e6.js", "assets/js/caaa529d.js", "assets/js/caed6a71.js", - "assets/js/cc11af99.js", "assets/js/cc73adb1.js", "assets/js/cc8ea217.js", "assets/js/ccc49370.js", @@ -7675,7 +5301,6 @@ "assets/js/cec3708f.js", "assets/js/ceca83dd.js", "assets/js/cf6cf838.js", - "assets/js/cf87428a.js", "assets/js/cfadd2be.js", "assets/js/cfed8efc.js", "assets/js/d0541a8a.js", @@ -7699,7 +5324,6 @@ "assets/js/d5103df8.js", "assets/js/d5673ba9.js", "assets/js/d571a224.js", - "assets/js/d59811b5.js", "assets/js/d5fb4741.js", "assets/js/d635ed10.js", "assets/js/d64ae8ec.js", @@ -7723,7 +5347,6 @@ "assets/js/dc07ab82.js", "assets/js/dc717843.js", "assets/js/dc9f502c.js", - "assets/js/dcc79e2c.js", "assets/js/dd4ae1cb.js", "assets/js/dd6a290d.js", "assets/js/dd7cebda.js", @@ -7735,9 +5358,11 @@ "assets/js/dfbb86b7.js", "assets/js/e027ab27.js", "assets/js/e068584e.js", + "assets/js/e162779c.js", "assets/js/e2616c8c.js", "assets/js/e2626920.js", "assets/js/e2a4b7ad.js", + "assets/js/e3823bc4.js", "assets/js/e417dd1f.js", "assets/js/e41876b0.js", "assets/js/e451dcc8.js", @@ -7767,32 +5392,32 @@ "assets/js/ec163545.js", "assets/js/ec8d6374.js", "assets/js/ecc16fc8.js", - "assets/js/ecc17e40.js", "assets/js/ecdd873d.js", "assets/js/ece2f253.js", - "assets/js/ecf274f0.js", - "assets/js/edbf3210.js", "assets/js/ee0be774.js", "assets/js/ee36cd22.js", "assets/js/ef315d05.js", "assets/js/ef8b811a.js", "assets/js/f0287a05.js", - "assets/js/f0369b89.js", "assets/js/f03c0a20.js", "assets/js/f0454fc5.js", "assets/js/f0582c44.js", "assets/js/f1063788.js", "assets/js/f13ccfdf.js", "assets/js/f1415acd.js", + "assets/js/f16262c2.js", "assets/js/f221cea9.js", "assets/js/f2389095.js", "assets/js/f33b7c63.js", "assets/js/f37d8fd3.js", + "assets/js/f384891b.js", "assets/js/f4b7a9b6.js", - "assets/js/f504faec.js", + "assets/js/f4cff03d.js", "assets/js/f5aba2ad.js", + "assets/js/f60fb5d4.js", "assets/js/f6c0ef84.js", "assets/js/f706513d.js", + "assets/js/f70cb098.js", "assets/js/f81c1134.js", "assets/js/f86e76d5.js", "assets/js/f87d1a86.js", @@ -7800,8 +5425,8 @@ "assets/js/f89a004b.js", "assets/js/f8cc9215.js", "assets/js/f9522484.js", - "assets/js/f970566f.js", "assets/js/f9a97f15.js", + "assets/js/f9c068af.js", "assets/js/f9cb7522.js", "assets/js/fa17a3e5.js", "assets/js/fa2b87d1.js", @@ -7819,7 +5444,6 @@ "assets/js/fcbb9f78.js", "assets/js/fd08327b.js", "assets/js/fd5f6740.js", - "assets/js/fd601be9.js", "assets/js/fd91423b.js", "assets/js/fdd5c367.js", "assets/js/fddc01cb.js", @@ -7883,7 +5507,6 @@ "captures/web-ui-home.html", "captures/wysiwyg-editor.desktop.html", "captures/wysiwyg-editor.mobile.html", - "docs/8.29/search-index.json", "docs/8.30/search-index.json", "docs/8.31/search-index.json", "docs/8.32/search-index.json", @@ -7893,6 +5516,7 @@ "docs/8.36/search-index.json", "docs/8.37/search-index.json", "docs/8.38/search-index.json", + "docs/9.10/search-index.json", "docs/9.8/search-index.json", "fiestapi-latest-version.txt", "img/air-fog-display.png", diff --git a/versioned_docs/version-9.14/deployment/_category_.json b/versioned_docs/version-9.14/deployment/_category_.json new file mode 100644 index 0000000000..ff8302792d --- /dev/null +++ b/versioned_docs/version-9.14/deployment/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Deployment", + "position": 6, + "link": { + "type": "generated-index", + "description": "Deploy FiestaBoard to production environments." + } +} diff --git a/versioned_docs/version-9.14/deployment/fiestaupdater.md b/versioned_docs/version-9.14/deployment/fiestaupdater.md new file mode 100644 index 0000000000..06eb5e07fa --- /dev/null +++ b/versioned_docs/version-9.14/deployment/fiestaupdater.md @@ -0,0 +1,270 @@ +--- +sidebar_position: 2 +description: "Technical reference for the FiestaUpdater sidecar — the companion container that powers FiestaBoard's one-click in-app updates." +keywords: [FiestaUpdater, fiestaupdater, in-app update, self-update sidecar, Docker socket, FiestaBoard updater] +--- + +# FiestaUpdater + +`fiestaupdater` is the companion sidecar container that powers FiestaBoard's +[in-app updates](/docs/features/updating). This page is a technical reference for the +sidecar itself — its architecture, configuration, HTTP API, and security model. + +:::tip Just want the Update Now button? +If you only want to enable in-app updates, see [In-App Updates](/docs/features/updating) +for the user-facing guide. This page is for understanding what the sidecar does and how +to configure it in custom Docker deployments. +::: + +## Why a separate container? + +The fiestaboard container can't safely update itself: as soon as +`docker compose up -d` recreates it, the running process (and any HTTP +response in flight) is killed. A side process is needed that: + +- Has access to the host's Docker socket +- Survives the recreation of the `fiestaboard` container +- Is not itself recreated as part of the update + +`fiestaupdater` is that process. It's a tiny container based on `docker:cli` +with a `socat`-driven HTTP listener and a single shell handler script. + +## Architecture + +```text +┌──────────────────────────────────────────────────────┐ +│ Docker host │ +│ │ +│ ┌──────────────────┐ internal network │ +│ │ fiestaboard │────────►┌────────────────┐ │ +│ │ (port 4420) │ POST │ fiestaupdater │ │ +│ │ │ /update│ (port 8765) │ │ +│ └──────────────────┘ Bearer └───────┬────────┘ │ +│ │ │ +│ │ docker.sock │ +│ ▼ │ +│ ┌────────────────────┐ │ +│ │ Docker daemon │ │ +│ │ pull + up -d │ │ +│ └────────────────────┘ │ +└──────────────────────────────────────────────────────┘ +``` + +Key properties: + +- **Internal-network only.** The sidecar's port is not published to the host — + it's reachable only by other containers on the same Docker Compose network. +- **Bearer-token authenticated.** Both containers share `FIESTAUPDATER_TOKEN` + via `.env`. Tokens are compared by SHA-256 hash to limit timing-attack surface. +- **Service-name allow-listed.** The compose service the sidecar is willing to + act on is validated against `^[a-z0-9_-]+$` before being passed to + `docker compose`, so even a compromised env var can't smuggle in shell metacharacters. + +## FiestaPi vs. Docker installs + +| Install type | Sidecar status | Auto-update default | +|---|---|---| +| **[FiestaPi](/docs/setup/raspberry-pi)** | Pre-configured, enabled by default | **On** — applies updates daily | +| **Docker / manual** | Off by default; opt in via `COMPOSE_PROFILES` | Off — banner shown, user clicks Update Now | + +On FiestaPi the `FIESTAUPDATER_TOKEN` is generated automatically on first boot +by `firstboot.sh`. On Docker installs you generate it yourself (see [Enabling on an existing Docker install](#enabling-on-an-existing-docker-install)). + +## Docker Compose configuration + +Below is the full `fiestaupdater` service definition used by +`docker-compose.hub.yml`. To enable in-app updates on a custom deployment, +add this service to your compose file (or pull it in via +`COMPOSE_PROFILES=fiestaupdater`): + +```yaml +services: + fiestaupdater: + image: fiestaboard/fiestaupdater:latest + container_name: fiestaupdater + profiles: ["fiestaupdater"] + restart: unless-stopped + pull_policy: always + environment: + - FIESTAUPDATER_TOKEN=${FIESTAUPDATER_TOKEN} + - FIESTAUPDATER_SERVICE=fiestaboard + - FIESTAUPDATER_COMPOSE_FILE=/compose/docker-compose.yml + - COMPOSE_PROJECT_NAME=fiestaboard + cap_add: + - SYS_BOOT # required for POST /shutdown + volumes: + # Talk to the host Docker daemon to pull images and recreate services. + - /var/run/docker.sock:/var/run/docker.sock + # Read the same compose file the user deployed with so we update the + # exact same service definition. + - ./docker-compose.hub.yml:/compose/docker-compose.yml:ro + # The compose file references `env_file: .env` for the fiestaboard + # service. When the sidecar runs `docker compose -f /compose/...`, + # Compose resolves `.env` relative to the compose file's directory. + # Without this mount, every pull/up/restart fails with + # "env file /compose/.env not found". + - ./.env:/compose/.env:ro + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:8765/healthz"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 5s +``` + +:::warning Three required volume mounts +All three volumes are required: + +1. `docker.sock` — to run `docker compose` against the host daemon +2. The compose file — so the sidecar updates exactly the service you deployed +3. `.env` — Compose resolves `env_file` relative to the compose file's directory + +Without the `.env` mount, every update silently no-ops with +`env file /compose/.env not found`. +::: + +### Enabling on an existing Docker install + +1. Generate a token and add it (along with `COMPOSE_PROFILES`) to your `.env`: + + ```bash + echo "COMPOSE_PROFILES=fiestaupdater" >> .env + echo "FIESTAUPDATER_TOKEN=$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" >> .env + ``` + +2. Make sure your compose file contains the `fiestaupdater` service block above. + (`docker-compose.hub.yml` already does.) + +3. Bring the stack up: + + ```bash + docker compose -f docker-compose.hub.yml up -d + ``` + +4. The Update Now button appears in **Settings → System** once the sidecar + passes its healthcheck. + +## Environment variables + +| Variable | Required | Default | Description | +|---|---|---|---| +| `FIESTAUPDATER_TOKEN` | yes | — | Shared bearer token. The sidecar refuses to start without it. Must match the value the `fiestaboard` container sees so the API can authenticate. | +| `FIESTAUPDATER_SERVICE` | no | `fiestaboard` | Compose service name to update. Validated against `^[a-z0-9_-]+$`; values that don't match fall back to `fiestaboard`. | +| `FIESTAUPDATER_COMPOSE_FILE` | no | `/compose/docker-compose.yml` | Path inside the sidecar container to the compose file. Mount your compose file here. | +| `FIESTAUPDATER_PORT` | no | `8765` | Port the sidecar listens on (internal network only). | +| `COMPOSE_PROJECT_NAME` | no | derived from compose dir | Must match the project name the main stack uses, so `docker compose` operates on the same containers. Set to `fiestaboard` to match `docker-compose.hub.yml`. | + +`COMPOSE_PROFILES=fiestaupdater` is set on the **host** (in your `.env`), +not inside the sidecar container — it tells Docker Compose to include the +sidecar service when bringing the stack up. + +## HTTP API + +All routes are served on port `8765` on the Docker Compose internal network. +None of them are reachable from the host. + +| Method | Path | Auth | Description | +|---|---|---|---| +| `GET` | `/healthz` | none | Liveness probe. Returns `{"status":"ok"}`. | +| `GET` | `/version` | none | Returns the running container's image and digest. Used by the UI to detect when an update has actually landed. | +| `GET` | `/last-update` | none | Returns the outcome of the most recent `/update` or `/rollback` attempt (`status`, `action`, `previous_digest`, `completed_at`, …). Returns `{"status":"none"}` when no attempt has been made. | +| `POST` | `/update` | bearer | `docker compose pull` + `up -d --no-deps` for the configured service. Records the pre-update digest + image reference in the state file so a later `/rollback` can revert to it. Returns `202 Accepted` immediately; the recreate runs in the background. | +| `POST` | `/rollback` | bearer | Body: `{"digest":"sha256:…","image":"repo:tag"}`. Retags the supplied digest onto the supplied image reference and runs `docker compose up -d --no-deps --force-recreate`. The digest must already be present locally. Returns `202`. | +| `POST` | `/restart` | bearer | `docker compose restart` for the configured service. Returns `202`. | +| `POST` | `/shutdown` | bearer | Stops the compose stack and powers off the host. Requires the `SYS_BOOT` capability. Returns `202`. | + +Authentication is `Authorization: Bearer `. Failed auth +returns `401`. Unknown routes return `404`. + +`POST /update`, `POST /rollback`, and `POST /restart` return `202` before the +work starts because the action will tear down the `fiestaboard` container +that originated the HTTP request — the connection would otherwise be cut +before any response could be flushed. + +### User-initiated rollback (5.1+) + +Updates are *not* rolled back automatically. The user decides if and when to +revert via the Settings UI, which calls the main `fiestaboard` API at +`POST /system/update/rollback`. + +The flow is: + +1. **At update time** — `POST /system/update` snapshots `data/*.json` to + `data/update-backups/pre-update-.json` (newest five retained) + *and* tags each snapshot with the running container's image digest + + reference (looked up via the sidecar's `/version`). The sidecar then + pulls + recreates as normal and persists `previous_digest` / + `previous_image` to `last-update.json` with `status: success`. +2. **At rollback time** — the user picks a snapshot. The main API + restores configuration from that snapshot via `BackupService` and then + POSTs `{"digest": "", "image": ""}` to + the sidecar's `/rollback`. The sidecar `docker tag`s the digest back + onto the image reference and `docker compose up -d --no-deps + --force-recreate`s the service so it boots on the rollback target. + +Both fields in the `/rollback` body are validated with strict regexes +(`^sha256:[a-f0-9]{64}$` for the digest; an explicit Docker-reference +allow-list for the image), and the body is size-capped at 8 KiB, so a +malformed call cannot lead to arbitrary `docker tag` invocations. + +The result is mirrored in `GET /system/update/status` as +`last_update_status`, `last_update_action`, `last_update_previous_digest`, +and `last_update_completed_at`, plus a `settings_snapshots` list (each with +its own recorded `previous_digest` / `previous_image`) so the UI can label +snapshots with the version they will roll back to. + +## Security + +- **No host port.** The sidecar's listener is bound to the compose-internal + network only. Even on a multi-tenant LAN, no other host can reach `/update`. +- **Bearer-token auth.** A 64-hex-character random token is required for every + state-changing route. SHA-256 hash comparison reduces timing-attack surface. +- **Service-name allow-list.** The configured service name must match + `^[a-z0-9_-]+$` before being passed to `docker compose`. No user input is + ever interpolated into a shell command. +- **Read-only mounts.** The compose file and `.env` are mounted `:ro`. +- **Limited blast radius.** The sidecar can pull images and recreate the one + service named in `FIESTAUPDATER_SERVICE`. It cannot start arbitrary + containers or run arbitrary shell commands. + +If you're uncomfortable mounting the Docker socket at all, leave the +`fiestaupdater` profile off and [update manually](/docs/features/updating#updating-manually-always-works). + +## Troubleshooting + +**The Update Now button doesn't appear.** The web UI probes the sidecar at +startup. Check: + +```bash +docker compose ps fiestaupdater # is it running and healthy? +docker logs fiestaupdater # any errors? +docker exec fiestaboard wget -qO- http://fiestaupdater:8765/healthz +``` + +The most common cause is `COMPOSE_PROFILES=fiestaupdater` missing from `.env`, +so the sidecar service is never started. + +**Update returns 202 but nothing happens.** Check the sidecar logs for +`env file /compose/.env not found` — this means the `.env` volume mount is +missing from your compose file. Add it (see [configuration](#docker-compose-configuration)) +and recreate the sidecar. + +**Update started but FiestaBoard never came back.** Wait a minute (the Pi can +take 60–90s to recreate the container), then reload +`http://fiestaboard.local:4420`. If still down: + +```bash +docker logs fiestaboard +docker compose -f docker-compose.hub.yml up -d +``` + +**I want to roll back.** Pin the previous image tag (e.g. +`image: fiestaboard/fiestaboard:5.1.1`) in your compose file, then +`docker compose up -d`. Automated rollback is planned for a future release. + +## See also + +- [In-App Updates](/docs/features/updating) — user-facing guide and Update Now flow +- [FiestaPi Quick Start](/docs/setup/raspberry-pi) — pre-configured Pi image with the sidecar enabled +- [Raspberry Pi Deployment](/docs/deployment/raspberry-pi) — running on an existing Pi with Docker +- [Environment Variables](/docs/reference/environment-variables#fiestaupdater) — full env-var reference diff --git a/versioned_docs/version-9.14/deployment/production.md b/versioned_docs/version-9.14/deployment/production.md new file mode 100644 index 0000000000..ce5bb15da8 --- /dev/null +++ b/versioned_docs/version-9.14/deployment/production.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 2 +description: "Production deployment best practices for running FiestaBoard reliably with Docker, SSL, and monitoring." +keywords: [FiestaBoard production, deployment guide, Docker production, SSL setup, self-hosted, best practices] +--- + +# Production Deployment + +Best practices for deploying FiestaBoard in a production environment. + +## Docker Compose for Production + +Use the production-optimized Docker Compose file: + +```bash +docker compose -f docker-compose.prod.yml up -d --build +``` + +Or use pre-built images from Docker Hub: + +```bash +docker compose -f docker-compose.hub.yml up -d +``` + +## Recommended Configuration + +### Environment Variables + +```bash +# In .env +REFRESH_INTERVAL_SECONDS=300 # Refresh every 5 minutes (default) +SILENCE_SCHEDULE_START_TIME=22:00 # Set quiet hours +SILENCE_SCHEDULE_END_TIME=07:00 +``` + +### Data Backup + +FiestaBoard stores all persistent state in two directories on the host: + +| Directory | Contents | +|-----------|----------| +| `data/` | Config, pages, schedules, carousels, settings, logs, and migration backups | +| `external_plugins/` | Marketplace plugins installed via the Integrations page | + +Both directories are automatically bind-mounted by Docker Compose. Back up both to fully preserve your setup: + +```bash +# One-shot backup (run from the directory containing your docker compose file) +tar -czf fiestaboard-backup-$(date +%Y%m%d).tar.gz data/ external_plugins/ + +# Automated daily backup via cron (runs at 2 AM) +0 2 * * * tar -czf /backups/fiestaboard-$(date +\%Y\%m\%d).tar.gz /path/to/FiestaBoard/data/ /path/to/FiestaBoard/external_plugins/ +``` + +**Restore from backup:** + +```bash +# Stop the container before restoring +docker compose down + +# Extract the backup (overwrites existing data/ and external_plugins/) +tar -xzf fiestaboard-backup-20240101.tar.gz + +# Start again — all pages, settings, and plugins are restored +docker compose up -d +``` + +:::tip What each file contains + +- `data/config.json` — Board API key and connection settings +- `data/settings.json` — Display preferences, active page, schedule +- `data/pages.json` — All board pages +- `data/schedules.json` / `data/carousels.json` — Automation rules +- `data/*.backup` — Automatic pre-migration snapshots (safe to delete after confirming an upgrade works) +::: + +## Updating + +```bash +# Pull latest changes +git pull + +# Rebuild and restart +docker compose -f docker-compose.prod.yml down +docker compose -f docker-compose.prod.yml up -d --build +``` + +:::tip Zero-Downtime Updates +Pull the latest code and rebuild the images first, then bring down the old containers and start the new ones. The downtime is minimal, just a few seconds while containers swap. +::: + +## Monitoring + +### Health Checks + +FiestaBoard provides a health endpoint: + +```bash +curl http://localhost:4420/api/health +``` + +You can use this with monitoring tools like Uptime Kuma or Healthchecks.io. + +### Logs + +```bash +# View all logs +docker compose logs -f + +# View FiestaBoard container logs only +docker compose logs -f fiestaboard +``` + +## Security Considerations + +- **Never commit `.env` files** to version control +- **Restrict API access** - The API has no authentication by default +- **Use a reverse proxy** - For external access, put FiestaBoard behind Nginx or Traefik with HTTPS +- **Keep Docker updated** - Regularly update Docker and Docker Compose + +## Running Behind a Reverse Proxy + +If you want to access FiestaBoard from outside your local network, use a reverse proxy with HTTPS: + +```nginx +server { + listen 443 ssl; + server_name fiestaboard.yourdomain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://localhost:4420; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +## Next Steps + +- [Raspberry Pi](/docs/deployment/raspberry-pi) - Deploy on a Raspberry Pi +- [Docker Setup](/docs/setup/docker-setup) - Docker architecture details +- [Cloud API](/docs/setup/cloud-api) - Remote board access via cloud API diff --git a/versioned_docs/version-9.14/deployment/raspberry-pi.md b/versioned_docs/version-9.14/deployment/raspberry-pi.md new file mode 100644 index 0000000000..3549053709 --- /dev/null +++ b/versioned_docs/version-9.14/deployment/raspberry-pi.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 1 +description: "Run FiestaBoard on a Raspberry Pi. Flash the FiestaPi image for zero-config setup, or use a Pi you already have with Docker." +keywords: [FiestaBoard Raspberry Pi, FiestaPi, Pi deployment, Raspberry Pi 4, Pi Zero 2W, self-hosted, split-flap Pi] +--- + +# Raspberry Pi + +Run FiestaBoard on a Raspberry Pi for a dedicated, always-on dashboard controller. + +## Recommended: Flash the FiestaPi image + +The fastest path is the **FiestaPi** pre-built image. Flash a microSD card, boot, open a browser — FiestaBoard is running and self-updating out of the box. No Docker setup, no command line. + +**→ [FiestaPi Quick Start](/docs/setup/raspberry-pi)** + +--- + +## Advanced: Install on an existing Pi with Docker + +Already have a Raspberry Pi running with Docker? You can run FiestaBoard the same way as any Docker install. + +### Compatible models + +| Model | Architecture | Status | +|-------|-------------|--------| +| Raspberry Pi 3B+ | `linux/arm64` | Supported (64-bit OS required) | +| Raspberry Pi Zero 2 W | `linux/arm64` | Supported (64-bit OS required) | +| Raspberry Pi 4 | `linux/arm64` | Supported | +| Raspberry Pi 5 | `linux/arm64` | Supported | + +> **Note:** 32-bit ARM (`linux/arm/v7`) is not supported. Node.js dropped ARM32v7 in v20+. Use 64-bit Raspberry Pi OS (Bookworm or later). + +### Install Docker (if needed) + +```bash +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +sudo usermod -aG docker $USER +# Log out and back in, then verify: +docker --version +``` + +### Start FiestaBoard + +Using pre-built images from Docker Hub (recommended — no build step): + +```bash +docker compose -f docker-compose.hub.yml up -d +``` + +Open **http://localhost:4420** in a browser on the same network. + +### Enable in-app updates + +Add these lines to your `.env` to enable the one-click Update Now button: + +```bash +COMPOSE_PROFILES=fiestaupdater +FIESTAUPDATER_TOKEN=$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n') +``` + +Then restart: `docker compose -f docker-compose.hub.yml up -d`. See [In-App Updates](/docs/features/updating) for more detail, or the [FiestaUpdater reference](/docs/deployment/fiestaupdater) for the full sidecar configuration. + +### Auto-start on boot + +Create a systemd unit so FiestaBoard starts automatically: + +```bash +# Enable Docker to start on boot +sudo systemctl enable docker + +# Create a systemd service for FiestaBoard +sudo tee /etc/systemd/system/fiestaboard.service > /dev/null << 'EOF' +[Unit] +Description=FiestaBoard +Requires=docker.service +After=docker.service network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/home/pi/FiestaBoard +ExecStart=/usr/bin/docker compose -f docker-compose.hub.yml up -d +ExecStop=/usr/bin/docker compose -f docker-compose.hub.yml down +TimeoutStartSec=300 + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl enable fiestaboard +sudo systemctl start fiestaboard +``` + +### Performance tips + +- **Use a fast SD card** — Class 10 or higher, ideally A2-rated +- **Consider USB boot** — Booting from a USB SSD significantly improves performance +- **Add a heatsink** — Sustained Docker workloads can throttle an uncooled Pi 3B +- **Set appropriate refresh intervals** - Don't refresh more frequently than needed + +```bash +# In .env, 60 seconds is usually sufficient +REFRESH_INTERVAL_SECONDS=60 +``` + +## Accessing From Other Devices + +Once FiestaBoard is running on your Pi, you can access it from any device on the same network: + +- **http://<pi-ip-address>:4420** — works with the default Docker bridge setup. Find your Pi's IP with `hostname -I`. +- **http://fiestapi.local:4420** — available on the FiestaPi image, whose host runs Avahi. Newly built images also announce FiestaBoard as a Bonjour HTTP service. + +For a separate Docker install, `MDNS_HOSTNAME` changes the container's advertised name only when it can reach the LAN. A bridge container cannot announce that name to the LAN. If you switch to host networking, nginx listens on port 3000; set `MDNS_PORT=3000` or provide a separate port 4420 proxy. + +## Next Steps + +- [Production Deployment](/docs/deployment/production) - Production best practices +- [Docker Setup](/docs/setup/docker-setup) - Docker architecture details diff --git a/versioned_docs/version-9.14/development/_category_.json b/versioned_docs/version-9.14/development/_category_.json new file mode 100644 index 0000000000..40542bb17d --- /dev/null +++ b/versioned_docs/version-9.14/development/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Development", + "position": 7, + "link": { + "type": "generated-index", + "description": "Develop plugins and contribute to FiestaBoard." + } +} diff --git a/versioned_docs/version-9.14/development/contributing.md b/versioned_docs/version-9.14/development/contributing.md new file mode 100644 index 0000000000..b73637b1b4 --- /dev/null +++ b/versioned_docs/version-9.14/development/contributing.md @@ -0,0 +1,109 @@ +--- +sidebar_position: 4 +description: "Contribute to FiestaBoard - learn about pull requests, coding standards, and how to help improve the project." +keywords: [FiestaBoard contributing, open source, pull request, contribution guide, GitHub, community] +--- + +# Contributing + +Guidelines for contributing to FiestaBoard. + +## Getting Started + +1. **Fork** the repository on GitHub +2. **Clone** your fork: + + ```bash + git clone https://github.com/your-username/FiestaBoard.git + cd FiestaBoard + ``` + +3. **Create a branch** for your changes: + + ```bash + git checkout -b feature/my-new-feature + ``` + +## Development Environment + +```bash +# Set up your environment +cp env.example .env +# Edit .env with your API keys + +# Start the development environment with hot reload +docker compose -f docker-compose.dev.yml up --build +``` + +The development environment provides: + +- **Hot reload** for Python backend changes +- **Hot reload** for React Router + Vite frontend changes +- Web UI at `http://localhost:4420` +- API at `http://localhost:4420` + +## Making Changes + +### Code Standards + +- **Python**: Follow PEP 8 and project pylint configuration +- **TypeScript/JavaScript**: Follow ESLint rules +- **Docker-first**: All features should work in Docker +- **No secrets**: Never commit API keys or sensitive data +- **Accessibility**: FiestaBoard aims for [WCAG 2.2 Level AA](/docs/reference/accessibility). Use semantic HTML, provide alt text, ensure sufficient color contrast (4.5:1 for normal text, 3:1 for large text and UI components), and support keyboard navigation. + +### Running Tests + +```bash +# Python tests +pytest tests/ + +# Web UI tests +npm run test:web + +# Lint web code +npm run lint:web +``` + +### Plugin Development + +When creating a new plugin: + +- Minimum **80% test coverage** is required +- Include `README.md` and `docs/SETUP.md` +- Follow the plugin template structure + +See the [Plugin Development Guide](/docs/development/plugin-guide) for details. + +## Submitting a Pull Request + +1. **Commit** your changes with clear commit messages +2. **Push** to your fork +3. **Open a Pull Request** against the `main` branch +4. **Describe** your changes in the PR description +5. **Squash commits** - PRs should be squashed into a single commit when merging + +### PR Checklist + +- [ ] Code follows project style guidelines +- [ ] Tests pass locally +- [ ] Documentation is updated if needed +- [ ] No secrets or API keys are committed +- [ ] Changes work in Docker + +## Reporting Issues + +- Use [GitHub Issues](https://github.com/Fiestaboard/FiestaBoard/issues) to report bugs +- Include steps to reproduce the issue +- Include relevant log output +- Specify your environment (OS, Docker version, etc.) + +## Security + +For security vulnerabilities, please report them responsibly. See the [SECURITY.md](https://github.com/Fiestaboard/FiestaBoard/blob/main/SECURITY.md) file for details. + +## Next Steps + +- [Local Development](/docs/setup/local-development) - Development setup details +- [Plugin Development Guide](/docs/development/plugin-guide) - Creating plugins +- [Testing Guide](/docs/development/testing) - Running and writing tests diff --git a/versioned_docs/version-9.14/development/plugin-ai.md b/versioned_docs/version-9.14/development/plugin-ai.md new file mode 100644 index 0000000000..c425ea9caa --- /dev/null +++ b/versioned_docs/version-9.14/development/plugin-ai.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 3 +description: "Call a language model from a FiestaBoard plugin with ai_complete(): it uses the AI providers set up in Settings, including signed-in OpenRouter, Hugging Face and ChatGPT, so the plugin carries no AI setup of its own." +keywords: [FiestaBoard AI plugin, ai_complete, FiestaBot providers, LLM plugin, generative plugin, ai_providers picker, plugin development] +--- + +# Using FiestaBoard's AI Providers + +A plugin that writes text with a language model does not need its own API key, endpoint, or sign-in. It calls `self.ai_complete(...)`, and FiestaBoard sends the request through the AI providers the user already set up in **Settings → AI Providers**, the same ones FiestaBot uses. That covers every protocol FiestaBoard speaks (OpenAI-compatible, Anthropic, and OpenAI Responses) and every way of connecting: a pasted key, or a sign-in with OpenRouter, Hugging Face, or ChatGPT. + +**Requires FiestaBoard 9.11.0 or later.** Set `"fiestaboard_version": ">=9.11.0"`, or guard the call as shown in [Older FiestaBoard](#older-fiestaboard). + +## The Short Version + +```python +from src.plugins.base import AIError, AINotConfiguredError, AIRejectedError, PluginBase, PluginResult + +def fetch_data(self) -> PluginResult: + try: + result = self.ai_complete( + [ + {"role": "system", "content": "You write one short line for a split-flap board."}, + {"role": "user", "content": "A greeting for a rainy Monday."}, + ], + provider_id=self.config.get("ai_provider") or None, # None = FiestaBot's default + max_tokens=60, + ) + except AINotConfiguredError as exc: + return PluginResult(available=False, error=f"Set up AI in Settings → AI Providers. ({exc})") + except AIRejectedError as exc: + return PluginResult(available=False, error=f"Reconnect the AI provider in Settings. ({exc})") + except AIError as exc: # unreachable, error answer, empty reply + return PluginResult(available=False, error=str(exc)) + return PluginResult(available=True, data={"line": result.text}) +``` + +## `ai_complete` + +```python +self.ai_complete( + messages, # "a prompt" or [{"role": "system"|"user"|"assistant", "content": str}, ...] + *, + provider_id=None, # None or "" = FiestaBot's default provider + model=None, # None = that provider's default model + temperature=None, # default 0.7; capped at 1.0 for Anthropic providers + max_tokens=None, # default 1500 + json=False, # True: ask for one JSON object and parse it into result.data + timeout=60.0, # seconds +) -> AICompletion +``` + +It blocks until the answer arrives, so it is safe to call from `fetch_data`. Async code awaits `self.ai_complete_async(...)` with the same arguments. + +The result has `.text`, `.model`, `.provider_id`, `.usage` (`prompt_tokens`, `completion_tokens`, `total_tokens`, any of which may be `None`), and `.data` (the parsed object when `json=True`). `str(result)` is the text. + +With `model=None` and no model saved for the provider (a provider added with **Sign in** is saved before a model is picked), FiestaBoard uses the first model the provider lists, as FiestaBot's chat does, and remembers it for ten minutes. + +A system message works with every protocol: for Anthropic it becomes the `system` field. With `json=True` FiestaBoard adds a system instruction asking for a single JSON object and extracts it from the reply even if the model wraps it in a code fence. It does not use a provider's JSON mode, because some local servers refuse it. + +## Errors + +Every failure is an `AIError`. Catch the three kinds separately to tell the user what to do: + +| Exception | When | What to tell the user | +|-----------|------|-----------------------| +| `AINotConfiguredError` | AI is turned off in Settings, no provider is set up, `provider_id` names a provider that no longer exists, or the provider has no model saved and lists none. Nothing is sent. | Set up or turn on AI in **Settings → AI Providers**, or pick another provider. | +| `AIRejectedError` | The provider refused the key or sign-in (`401`), or a signed-in provider must be signed in again. A signed-in provider's `401` is retried once with a refreshed token before this is raised. | Reconnect the provider in **Settings → AI Providers**. | +| `AIProviderError` | The provider could not be reached, answered with an error, returned an empty reply, or (with `json=True`) returned something that is not a JSON object. | Try again later. The message carries the provider's own error text. | + +A malformed `messages` argument raises `ValueError`: that is a bug in the plugin, not something to show the user. + +Import the exceptions from `src.plugins.base` (or `src.ai.plugin_api`). `AIError` is the same class as core's `AIGenerationError`. + +Don't call `ai_complete` on every render. Cache the result and refresh it on your plugin's own schedule; model calls are slow and may cost the user money. + +## Letting the User Pick a Provider + +Add a settings field with the `ai_providers` picker. Core answers it for you, so you don't write `get_options` for it: + +```json +"ai_provider": { + "type": "string", + "title": "AI provider", + "description": "Leave empty for FiestaBot's default provider.", + "default": "", + "ui:widget": "remote-options", + "ui:options": {"options_id": "ai_providers", "searchable": true, "cache_seconds": 30, "placeholder": "FiestaBot's default provider"} +} +``` + +The picker lists every provider of every protocol, with its protocol, its default model, and whether it connects by signing in. When AI is turned off or nothing is set up, the field shows why instead of an empty list. Pass the saved value as `provider_id`; an empty string means the default provider. + +If your plugin already implements `get_options` for other fields, end it with `return super().get_options(request)` so `ai_providers` still reaches core. `self.ai_providers()` returns the same list as dictionaries (`id`, `name`, `protocol`, `model`, `models`, `default`, `sign_in`), with no keys or tokens, for a plugin that wants to build its own. + +## Keeping an Existing API Key Working + +A plugin that used to take its own `api_key` (and `api_base_url`, `model`) must keep working for users who saved one. Use the saved key when it is set, exactly as before, and use `ai_complete` only when it is empty. Never migrate or delete the old settings. + +```python +if self.config.get("api_key"): + text = self._call_with_own_key() # unchanged code path +else: + text = self.ai_complete(prompt, provider_id=self.config.get("ai_provider") or None).text +``` + +Don't add an OAuth sign-in to a plugin for an AI service. Signing in to OpenRouter, Hugging Face, or ChatGPT happens once, in **Settings → AI Providers**, and every plugin shares it. + +## Testing + +Replace the method on the plugin instance; no network is involved: + +```python +from src.ai.plugin_api import AICompletion, AIRejectedError + +def test_writes_the_line(plugin): + plugin.ai_complete = lambda *a, **k: AICompletion(text="HELLO", model="test-model", provider_id="test") + assert plugin.fetch_data().data["line"] == "HELLO" + +def test_rejected_sign_in_is_unavailable(plugin): + def boom(*a, **k): + raise AIRejectedError("Sign in to Test again in Settings → AI.") + plugin.ai_complete = boom + assert plugin.fetch_data().available is False +``` + +## Older FiestaBoard {#older-fiestaboard} + +Before 9.11.0 there is no `ai_complete`. A plugin that must load on older cores checks first: + +```python +complete = getattr(self, "ai_complete", None) +if complete is None: + return PluginResult(available=False, error="Needs FiestaBoard 9.9 or later, or an API key in this plugin's settings.") +``` + +Import the exceptions inside the same guard, since `src.plugins.base` has no `AIError` before 9.11.0. diff --git a/versioned_docs/version-9.14/development/plugin-guide.md b/versioned_docs/version-9.14/development/plugin-guide.md new file mode 100644 index 0000000000..1aca3b5800 --- /dev/null +++ b/versioned_docs/version-9.14/development/plugin-guide.md @@ -0,0 +1,1138 @@ +--- +sidebar_position: 1 +description: "End-to-end guide to creating, testing, and submitting a FiestaBoard plugin - from initial idea to merged pull request." +keywords: [FiestaBoard plugin development, create plugin, plugin API, custom plugin, developer guide, Python plugin, PluginBase, manifest.json] +--- + +# Plugin Development Guide + +This guide walks you through creating a FiestaBoard plugin from scratch - from initial idea through development, testing, and submitting a pull request to get it merged into the project. + +## What Is a Plugin? + +A FiestaBoard plugin is a self-contained Python package that fetches data from an external source (an API, a local service, or computed values) and exposes that data as **template variables**. Users reference those variables in the page editor to build dynamic displays for their split-flap board. + +Every plugin has four required components: + +| Component | File | Purpose | +|-----------|------|---------| +| **Manifest** | `manifest.json` | Metadata, configuration schema, and declared variables | +| **Implementation** | `__init__.py` | Python class that inherits from `PluginBase` and fetches data | +| **Tests** | `tests/test_plugin.py` | Automated tests with ≥80% code coverage | +| **Documentation** | `README.md` + `docs/SETUP.md` | Developer docs and user-facing setup guide | + +### Where Plugins Live + +FiestaBoard discovers plugins from three sources: + +| Source | Location | Naming Convention | +|--------|----------|-------------------| +| **Built-in** | `plugins/` directory | Directory name must match `manifest.json` `id` field | +| **Registry** | Cloned into `external_plugins/` | Repository must be named `fiestaboard-plugin--{name}` | +| **Custom Git** | Cloned into `external_plugins/` | No naming convention required | + +Built-in plugins are auto-discovered at startup. External plugins (registry and custom git) are cloned into the `external_plugins/` directory and also auto-discovered. + +:::info Plugin Sources +If you want your plugin included in the curated **plugin registry**, the git repository must follow the `fiestaboard-plugin--{name}` naming convention (e.g. `fiestaboard-plugin--my-weather`). This is **not** required for personal or custom plugins you install via a git URL. +::: + +--- + +## Step 1: Set Up Your Development Environment + +Before writing any code, get the dev environment running: + +```bash +# Clone the repo (or your fork) +git clone https://github.com/Fiestaboard/FiestaBoard.git +cd FiestaBoard + +# Create a feature branch +git checkout -b feat-plugin-my-plugin + +# Copy the environment template +cp env.example .env +# Edit .env with your board API key (see README for details) + +# Start the development stack (hot reload for Python and the React Router + Vite web UI) +docker compose -f docker-compose.dev.yml up --build +``` + +The dev environment gives you: + +- **Web UI** at `http://localhost:4420` (hot reloads on frontend changes) +- **API** at `http://localhost:4420` (auto-reloads on Python changes, proxied via nginx) + +See [Local Development](/docs/setup/local-development) for full setup details. + +--- + +## Step 2: Scaffold Your Plugin + +Copy the built-in template to create your plugin skeleton: + +```bash +cp -r plugins/_template plugins/my_plugin +``` + +This gives you the required directory structure: + +```text +plugins/my_plugin/ +├── __init__.py # Plugin class (PluginBase subclass) +├── manifest.json # Plugin metadata and configuration schema +├── README.md # Developer-focused documentation +├── docs/ +│ └── SETUP.md # User-facing setup guide (API keys, config) +└── tests/ + ├── __init__.py # Required (can be empty) + ├── conftest.py # Shared test fixtures + └── test_plugin.py # Plugin tests (≥80% coverage required) +``` + +:::warning Important +The plugin directory name **must** match the `id` field in `manifest.json`. If your directory is `plugins/my_plugin/`, then `manifest.json` must have `"id": "my_plugin"`. +::: + +--- + +## Step 3: Define Your Manifest + +The `manifest.json` file is the heart of your plugin. It tells FiestaBoard what your plugin is, how it's configured, and what template variables it provides. + +### Minimal Example + +```json +{ + "id": "my_plugin", + "name": "My Plugin", + "version": "1.0.0", + "description": "A brief description of what this plugin does", + "author": "Your Name", + "icon": "puzzle", + "category": "utility", + "settings_schema": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "title": "API Key", + "description": "Your API key from example.com", + "ui:widget": "password" + } + }, + "required": ["api_key"] + }, + "variables": { + "simple": ["value", "status"] + }, + "max_lengths": { + "value": 10, + "status": 15 + } +} +``` + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique identifier - lowercase letters, digits, and underscores only. Must start with a letter. Must match directory name. | +| `name` | string | Human-readable name shown in the UI (max 50 characters). | +| `version` | string | Semantic version in `X.Y.Z` format (e.g., `"1.0.0"`). | + +### Strongly Recommended Fields + +These fields are not enforced by the manifest validator but should be included in every plugin: + +| Field | Type | Description | +|-------|------|-------------| +| `description` | string | Short description (max 200 characters). | +| `author` | string | Plugin author or maintainer. | +| `settings_schema` | object | [JSON Schema](https://json-schema.org/) defining user-configurable settings. | +| `variables` | object | Template variables your plugin exposes (see [Variables](#variables) below). | +| `max_lengths` | object | Maximum character lengths for each variable (used for template validation). | +| `teaser` | string | One line of board text for your plugin's directory card (see [Board Previews](#board-previews)). | +| `previews` | array | Literal boards shown on your plugin's detail page (see [Board Previews](#board-previews)). | + +### Optional Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `icon` | string | `"puzzle"` | [Lucide](https://lucide.dev/icons/) icon name for the UI. | +| `category` | string | `"utility"` | Grouping category. Valid values: `"art"`, `"data"`, `"transit"`, `"weather"`, `"entertainment"`, `"utility"`, `"home"`. | +| `repository` | string | - | GitHub repository URL. | +| `documentation` | string | `"README.md"` | Path to documentation file relative to plugin directory. | +| `env_vars` | array | `[]` | Environment variables the plugin can read (see [Environment Variables](#environment-variables)). | +| `color_rules_schema` | object | `{}` | Schema for dynamic color rules (see [Color Rules](#color-rules)). | +| `oauth` | object | - | The OAuth provider the plugin signs in to. The platform runs the sign-in and stores the tokens (see [Signing In with OAuth](/docs/development/plugin-oauth)). | +| `fiestaboard_version` | string | - | Minimum FiestaBoard version, such as `">=9.5.0"`. Set it when you rely on a newer platform feature. | + +### Board Previews {#board-previews} + +The plugin directory renders your plugin as a real split-flap board rather than a screenshot. You supply the content as text; nothing needs to be photographed, and no images are committed. + +Two fields: + +```json +{ + "teaser": "{66}AAPL +1.88%", + "previews": [ + { + "device_type": "flagship", + "rows": [ + " STOCK PRICES ", + "{66}AAPL 189.84 +1.2%", + "{63}TSLA 248.50 -0.8%" + ] + }, + { + "device_type": "note", + "rows": [ + " STOCK PRICES ", + "{66}AAPL +1.2%", + "{63}TSLA -0.8%" + ] + } + ] +} +``` + +**`teaser`** is one line shown on your plugin's card in the directory. Maximum **15 tiles** — the Note width, so the same teaser fits every board. + +**`previews`** is a list of boards, each declaring its own shape. Add one per board family you have composed for. `previews[0]` is the hero on your detail page. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `label` | string | derived from shape | Tab label, e.g. `"Morning"`. | +| `device_type` | string | `"flagship"` | `"flagship"` (22×6), `"note"` (15×3), or `"note_array"`. | +| `notes_wide` | integer | `1` | Notes across, `note_array` only. Max 4 for previews. | +| `notes_tall` | integer | `1` | Notes down, `note_array` only. Max 4 for previews. | +| `rows` | array | required | Literal board rows. | + +#### Counting tiles, not characters + +A colour marker occupies **one flap**, however many characters it takes to write. `{66}` is one tile, not four. Closing tags like `{/green}` are formatting only and occupy none. + +```text +"{66}AAPL +1.88%" → 12 tiles (1 colour flap + 11 characters) +"{green}AAPL{/green}" → 5 tiles (1 colour flap + 4 characters) +``` + +This is why a 22-tile flagship row can be far longer than 22 characters of source text. + +#### Rules + +- Rows may be **shorter** than the board's row count — the remainder is padded with blanks. They may never be longer. +- Every row must fit the board's width in tiles. +- Content must be **literal**. A `{{variable}}` reference is rejected: previews render with no plugin data to resolve against. +- Characters the board cannot display are rejected rather than silently replaced with blanks. +- Lowercase is fine — the board uppercases on render. +- Transition plugins do not declare these fields. + +Run `python scripts/validate_plugins.py --verbose` to check your previews; every rule above reports a specific, actionable error. + +### Settings Schema + +The `settings_schema` uses [JSON Schema](https://json-schema.org/) with UI widget extensions. This schema drives the configuration form in the web UI's Integrations page. + +```json +{ + "settings_schema": { + "type": "object", + "properties": { + "api_key": { + "type": "string", + "title": "API Key", + "description": "Get your key from example.com", + "ui:widget": "password" + }, + "provider": { + "type": "string", + "title": "Provider", + "enum": ["option1", "option2"], + "default": "option1" + }, + "locations": { + "type": "array", + "title": "Locations", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "title": "Name" }, + "value": { "type": "string", "title": "Value" } + }, + "required": ["name", "value"] + } + }, + "refresh_seconds": { + "type": "integer", + "title": "Refresh Interval (seconds)", + "default": 300, + "minimum": 60, + "maximum": 3600 + } + }, + "required": ["api_key"] + } +} +``` + +**Available UI widgets:** + +| Widget | Usage | +|--------|-------| +| `password` | Masked input for secrets and API keys | +| `textarea` | Multi-line text input | +| `select` | Dropdown (automatically used for `enum` fields) | +| `timezone` | Timezone picker with autocomplete | + +### Color Rules {#color-rules} + +`color_rules_schema` declares which variables support automatic color coding. When a rule fires, FiestaBoard prepends a color tile before the variable value in the template. No plugin code is required — the template engine handles it automatically. + +```json +{ + "color_rules_schema": { + "status": { + "type": "threshold", + "description": "Color based on status value", + "default_rules": [ + { "condition": "==", "value": "OK", "color": "green" }, + { "condition": "==", "value": "WARNING", "color": "yellow" }, + { "condition": "==", "value": "CRITICAL", "color": "red" } + ] + }, + "temp_f": { + "type": "threshold", + "description": "Color based on temperature", + "default_rules": [ + { "condition": ">=", "value": 90, "color": "red" }, + { "condition": ">=", "value": 70, "color": "yellow" }, + { "condition": "<", "value": 70, "color": "blue" } + ] + } + } +} +``` + +Each top-level key is a variable field name. The value is an object with: + +| Key | Required | Description | +|-----|----------|-------------| +| `type` | No | Informational label (e.g., `"threshold"`, `"exact"`) — not enforced by the engine | +| `description` | No | Human-readable description shown in the UI | +| `default_rules` | No | Rules applied when the user has not set custom rules | + +Each rule object has three fields: + +| Field | Description | +|-------|-------------| +| `condition` | Comparison operator: `==`, `!=`, `>`, `<`, `>=`, `<=` | +| `value` | Value to compare against (number or string) | +| `color` | One of: `red`, `orange`, `yellow`, `green`, `blue`, `violet`, `white`, `black` | + +Rules are checked in order. The first match wins; the rest are ignored. Numeric operators (`>`, `<`, `>=`, `<=`) coerce both sides to float. `==` and `!=` always do case-insensitive string comparison. + +**Using color rules in templates** + +Reference the variable as normal — color is applied automatically: + +```jinja +{{my_plugin.status}} +{{my_plugin.temp_f}}°F +``` + +If `status` is `"WARNING"`, the board shows a yellow tile followed by `WARNING`. No special template syntax is needed. + +> **Note:** A colored line takes 2 characters of board width (tile + space). FiestaBoard accounts for this in max-length calculations automatically. + +**User overrides** + +Users can set their own rules per field in the FiestaBoard web UI. Custom rules replace the `default_rules` entirely. The `color_rules_schema` tells the UI which fields are eligible for color customization. Fields without `default_rules` appear in the UI but start with no rules applied. + +### Variables {#variables} + +The `variables` object declares what template variables your plugin provides. There are three types: + +#### Simple Variables + +Top-level key-value pairs. Users reference them as `{{my_plugin.temperature}}`. + +```json +{ + "variables": { + "simple": ["temperature", "humidity", "condition"] + } +} +``` + +#### Array Variables + +Indexed collections. Users reference items as `{{my_plugin.locations.0.temperature}}`. + +```json +{ + "variables": { + "arrays": { + "locations": { + "label_field": "name", + "item_fields": ["name", "temperature", "humidity"] + } + } + } +} +``` + +- `label_field` - the field used as a human-readable label in the UI. +- `item_fields` - all fields available on each array item. + +Like `simple`, `item_fields` can also be an object that maps each field to its metadata (`description`, `type`, `max_length`, `group`, `example`): + +```json +{ + "variables": { + "groups": { "game": { "label": "Game" } }, + "arrays": { + "games": { + "label_field": "formatted", + "item_fields": { + "formatted": { "description": "One-line game summary", "group": "game" }, + "minutes_until_game": { + "description": "Minutes until first pitch", + "type": "number", + "max_length": 4, + "group": "game" + } + } + } + } + } +} +``` + +The editor shows each `description` as a tooltip in the variable picker and in the plugin's Template Variables table, and the table sorts fields into their `group`. A field's `max_length` counts as a `max_lengths` entry for `games.*.minutes_until_game`. If you also set that key in `max_lengths`, the `max_lengths` value wins. The same object form works for `item_fields` inside `sub_arrays`. + +#### Nested Arrays (Arrays within Arrays) + +For data like transit stops that contain multiple lines. Users reference them as `{{my_plugin.stops.0.lines.N.next_arrival}}`. + +```json +{ + "variables": { + "arrays": { + "stops": { + "label_field": "stop_name", + "item_fields": ["stop_name", "stop_code"], + "sub_arrays": { + "lines": { + "key_type": "dynamic", + "key_field": "line", + "item_fields": ["line", "next_arrival", "is_delayed"] + } + } + } + } + } +} +``` + +### Max Lengths + +Every variable should have a corresponding max-length entry. This helps the page editor validate that content fits the board's 22-character-wide display. + +```json +{ + "max_lengths": { + "temperature": 3, + "condition": 15, + "locations.*.name": 10, + "locations.*.temperature": 3 + } +} +``` + +The `*` wildcard matches any array index. + +### Environment Variables + +If your plugin can read configuration from environment variables (as a fallback to UI config), declare them: + +```json +{ + "env_vars": [ + { + "name": "MY_PLUGIN_API_KEY", + "required": false, + "description": "API key (can also be set in the web UI)" + } + ] +} +``` + +--- + +## Step 4: Implement Your Plugin + +Your plugin class lives in `__init__.py` and must inherit from `PluginBase`. Here's the minimal contract: + +```python +"""My Plugin for FiestaBoard.""" + +import logging +import os +from typing import Any, Dict, List + +from src.plugins.base import PluginBase, PluginResult + +logger = logging.getLogger(__name__) + + +class MyPlugin(PluginBase): + """Fetches data from My Service.""" + + @property + def plugin_id(self) -> str: + """Return the plugin ID - must match manifest.json 'id' field.""" + return "my_plugin" + + def fetch_data(self) -> PluginResult: + """Fetch data and return it as template variables.""" + api_key = self.config.get("api_key") or os.getenv("MY_PLUGIN_API_KEY") + + if not api_key: + return PluginResult(available=False, error="API key not configured") + + try: + # Your data-fetching logic here + data = {"value": "123", "status": "OK"} + + return PluginResult(available=True, data=data) + + except Exception as e: + logger.error("Error fetching data: %s", e, exc_info=True) + return PluginResult(available=False, error=str(e)) + + def validate_config(self, config: Dict[str, Any]) -> List[str]: + """Validate configuration. Return a list of error messages (empty if valid).""" + errors = [] + if not config.get("api_key"): + errors.append("API key is required") + return errors +``` + +### PluginBase API Reference + +Your plugin inherits these from `PluginBase`: + +| Member | Type | Description | +|--------|------|-------------| +| `plugin_id` | property (abstract) | **Required.** Return your plugin's unique ID. | +| `fetch_data()` | method (abstract) | **Required.** Fetch data and return a `PluginResult`. | +| `validate_config(config)` | method | Validate a config dict. Return list of error strings. Default returns `[]`. | +| `cleanup()` | method | Called when plugin is disabled. Override to release resources. | +| `on_config_change(old, new)` | method | Called when config is updated. Override to reset caches. | +| `get_formatted_display()` | method | Return 6 pre-formatted lines for "single page" mode. Default returns `None`. | +| `config` | property | The current configuration dictionary (set by the platform). | +| `enabled` | property | Whether the plugin is currently enabled. | +| `manifest` | property | The raw manifest dictionary. | +| `refresh_seconds` | property | Effective refresh interval in seconds (from manifest + config). Returns `None` if not defined. | +| `get_data()` | method | Get plugin data with automatic caching based on `refresh_seconds`. This is what the platform calls. | +| `clear_cache()` | method | Clear cached data, forcing a fresh fetch on the next `get_data()` call. | +| `get_variables_schema()` | method | Returns the `variables` section from the manifest. | +| `get_max_lengths()` | method | Returns the `max_lengths` section from the manifest. | +| `get_settings_schema()` | method | Returns the `settings_schema` section from the manifest. | +| `get_env_vars()` | method | Returns environment variable definitions from the manifest. | +| `get_oauth_token()` | method | Returns the current access token for the provider in the manifest's `oauth` block, or `None` if the user is not signed in. Call it on every fetch (see [Signing In with OAuth](/docs/development/plugin-oauth)). | +| `ai_complete(messages, *, provider_id=None, model=None, temperature=None, max_tokens=None, json=False)` | method | **9.11.0.** Asks one of the AI providers set up in Settings → AI Providers and returns an `AICompletion` (`.text`, `.data`, ...). Raises `AINotConfiguredError`, `AIRejectedError`, or `AIProviderError`. See [Using FiestaBoard's AI Providers](/docs/development/plugin-ai). | +| `ai_providers()` | method | **9.11.0.** The AI providers set up in Settings, without keys or tokens. A settings field with `"options_id": "ai_providers"` gets them as a picker with no code. | + +### PluginResult + +`fetch_data()` must return a `PluginResult`: + +```python +@dataclass +class PluginResult: + available: bool # True if data was fetched successfully + data: Optional[Dict[str, Any]] = None # Template variables + error: Optional[str] = None # Error message if fetch failed + formatted_lines: Optional[List[str]] = None # Optional pre-formatted 6-line display +``` + +- Set `available=True` and populate `data` on success. +- Set `available=False` and populate `error` on failure. +- `formatted_lines` is optional - provide 6 strings (one per board row) if your plugin supports a standalone display mode. + +### Accessing Configuration + +Use `self.config` (a `Dict[str, Any]`) to read values the user set in the web UI: + +```python +def fetch_data(self) -> PluginResult: + api_key = self.config.get("api_key") + location = self.config.get("location", "San Francisco") + refresh = self.config.get("refresh_seconds", 300) + # ... +``` + +You can also fall back to environment variables: + +```python +api_key = self.config.get("api_key") or os.getenv("MY_PLUGIN_API_KEY") +``` + +### Constructor + +`PluginBase.__init__` accepts a `manifest` dictionary (the parsed `manifest.json`). You usually don't need to override `__init__`, but if you do, call `super().__init__(manifest)`: + +```python +def __init__(self, manifest: Dict[str, Any]): + super().__init__(manifest) + self._cache = None +``` + +### Real-World Example: Date & Time Plugin + +This plugin has no external API - it uses Python's `datetime` library: + +```python +"""Date & Time plugin for FiestaBoard.""" + +from typing import Any, Dict, List, Optional +import logging +from datetime import datetime + +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +from src.plugins.base import PluginBase, PluginResult + +logger = logging.getLogger(__name__) + + +class DateTimePlugin(PluginBase): + """Provides current date/time in the configured timezone.""" + + @property + def plugin_id(self) -> str: + return "date_time" + + def validate_config(self, config: Dict[str, Any]) -> List[str]: + errors = [] + timezone = config.get("timezone", "America/Los_Angeles") + try: + ZoneInfo(timezone) + except (ZoneInfoNotFoundError, ValueError): + errors.append(f"Invalid timezone: {timezone}") + return errors + + def fetch_data(self) -> PluginResult: + try: + tz = ZoneInfo(self.config.get("timezone", "America/Los_Angeles")) + now = datetime.now(tz) + return PluginResult( + available=True, + data={ + "date": now.strftime("%Y-%m-%d"), + "time": now.strftime("%H:%M"), + "day_of_week": now.strftime("%A"), + "time_12h": now.strftime("%I:%M %p").lstrip("0"), + # ... more variables + }, + ) + except Exception as e: + logger.exception("Error fetching datetime data") + return PluginResult(available=False, error=str(e)) +``` + +--- + +## Step 5: Write Tests + +**All plugins must include tests with ≥80% code coverage.** CI will fail if coverage is below this threshold. + +### Test Structure + +```text +plugins/my_plugin/tests/ +├── __init__.py # Required (can be empty) +├── conftest.py # Shared test fixtures +└── test_plugin.py # Your tests (filenames must start with test_) +``` + +### conftest.py + +Use the template's `conftest.py` or create your own with shared fixtures: + +```python +"""Plugin test fixtures.""" + +import pytest +from src.plugins.testing import create_mock_response + + +@pytest.fixture(autouse=True) +def reset_plugin_singletons(): + """Reset plugin singletons before each test.""" + yield + + +@pytest.fixture +def mock_api_response(): + """Fixture to create mock API responses.""" + return create_mock_response + + +@pytest.fixture +def sample_config(): + """Sample configuration for testing.""" + return { + "api_key": "test_api_key_12345", + "refresh_seconds": 300, + } +``` + +### Writing Tests + +Test the key behaviors: successful fetches, missing configuration, API errors, edge cases, and config validation. + +```python +"""Tests for my_plugin.""" + +import pytest +from unittest.mock import patch + +from plugins.my_plugin import MyPlugin +from src.plugins.base import PluginResult +from src.plugins.testing import PluginTestCase, create_mock_response + + +class TestMyPlugin(PluginTestCase): + """Core plugin tests.""" + + def _make_plugin(self, config=None): + """Helper to create a configured plugin instance.""" + manifest = { + "id": "my_plugin", + "name": "My Plugin", + "version": "1.0.0", + } + plugin = MyPlugin(manifest) + if config: + plugin.config = config + return plugin + + def test_plugin_id(self): + """Plugin ID must match the directory name.""" + plugin = self._make_plugin() + assert plugin.plugin_id == "my_plugin" + + def test_fetch_data_success(self): + """Successful fetch returns available=True with data.""" + plugin = self._make_plugin({"api_key": "test_key"}) + + with patch("requests.get") as mock_get: + mock_get.return_value = create_mock_response( + data={"value": "42", "status": "OK"} + ) + result = plugin.fetch_data() + + assert result.available is True + assert result.error is None + assert "value" in result.data + + def test_fetch_data_missing_config(self): + """Missing API key returns available=False.""" + plugin = self._make_plugin({}) + result = plugin.fetch_data() + + assert result.available is False + assert result.error is not None + + @patch("requests.get") + def test_fetch_data_network_error(self, mock_get): + """Network errors are handled gracefully.""" + mock_get.side_effect = Exception("Connection refused") + plugin = self._make_plugin({"api_key": "test_key"}) + result = plugin.fetch_data() + + assert result.available is False + assert "Connection refused" in result.error + + def test_validate_config_valid(self): + """Valid config passes validation.""" + plugin = self._make_plugin() + errors = plugin.validate_config({"api_key": "key123"}) + assert len(errors) == 0 + + def test_validate_config_missing_required(self): + """Missing required fields are caught.""" + plugin = self._make_plugin() + errors = plugin.validate_config({}) + assert len(errors) > 0 + assert any("api_key" in e.lower() for e in errors) + + def test_data_variables_match_manifest(self): + """Returned data keys match the variables declared in manifest.json.""" + import json + from pathlib import Path + + manifest_path = Path(__file__).parent.parent / "manifest.json" + with open(manifest_path) as f: + manifest = json.load(f) + + declared_vars = manifest["variables"]["simple"] + plugin = self._make_plugin({"api_key": "test_key"}) + + with patch("requests.get") as mock_get: + mock_get.return_value = create_mock_response(data={"value": "1", "status": "OK"}) + result = plugin.fetch_data() + + if result.available: + for var in declared_vars: + assert var in result.data, f"Variable '{var}' declared in manifest but missing from data" + + +class TestMyPluginEdgeCases: + """Edge case and error handling tests.""" + + def test_empty_api_response(self): + """Empty API response is handled gracefully.""" + # ... + + def test_timeout_handling(self): + """Request timeouts produce a clean error.""" + # ... +``` + +### Running Tests + +```bash +# Test a single plugin (with coverage) +docker compose exec fiestaboard python scripts/run_plugin_tests.py --plugin=my_plugin + +# Or using pytest directly +docker compose exec fiestaboard pytest plugins/my_plugin/tests/ -v + +# Run with coverage report +docker compose exec fiestaboard pytest plugins/my_plugin/tests/ \ + --cov=plugins/my_plugin --cov-report=term-missing + +# Run all plugin tests +docker compose exec fiestaboard python scripts/run_plugin_tests.py --verbose + +# Dry run (see what would be tested) +docker compose exec fiestaboard python scripts/run_plugin_tests.py --dry-run +``` + +### Coverage Requirements + +| Requirement | Value | +|-------------|-------| +| Minimum coverage | **80%** per plugin | +| CI enforcement | Yes - builds fail below threshold | +| Exclusions | Use `# pragma: no cover` sparingly for legitimately untestable code | + +--- + +## Step 6: Write Documentation + +Each plugin needs two documentation files: + +### README.md (Developer-Focused) + +Explain how the plugin works internally - data sources, API details, architecture decisions. This is for someone reading the code or contributing improvements. + +### docs/SETUP.md (User-Focused) + +Walk a user through enabling and configuring the plugin. Include: + +- How to obtain any required API keys (with links) +- Step-by-step configuration in the web UI +- Available template variables and example usage +- Screenshots of the plugin in action (place images in `docs/`) +- Troubleshooting tips +- API rate limits and plan details + +Use the template's `docs/SETUP.md` as a starting point. + +--- + +## Step 7: Validate Your Plugin + +Before submitting, run the validation scripts to catch common issues: + +```bash +# Validate all plugin manifests (structure, schema, naming) +docker compose exec fiestaboard python scripts/validate_plugins.py --verbose + +# Run your plugin's tests with coverage +docker compose exec fiestaboard python scripts/run_plugin_tests.py --plugin=my_plugin + +# Verify the plugin loads and appears in the API +curl http://localhost:4420/api/plugins +curl http://localhost:4420/api/plugins/my_plugin +curl http://localhost:4420/api/plugins/my_plugin/data +``` + +The validator checks: + +- `id` matches directory name and follows the naming rules (lowercase, underscores, starts with letter) +- `version` follows semantic versioning (`X.Y.Z`) +- `category` is a valid value +- Required files exist (`__init__.py`, `manifest.json`) +- `settings_schema` and `variables` are well-formed +- No duplicate plugin IDs + +--- + +## Step 8: Test in the Web UI + +With the dev stack running, open `http://localhost:4420` and: + +1. Go to **Integrations** - your plugin should appear in the list +2. **Enable** the plugin and enter any required configuration +3. **Create a page** using your plugin's template variables (e.g., `{{my_plugin.value}}`) +4. Verify the page renders correctly with live data + +--- + +## Step 9: Submit Your Pull Request + +### Pre-Submission Checklist + +Before opening a PR, verify every item: + +- [ ] Plugin directory name matches `manifest.json` `id` +- [ ] `manifest.json` has all required fields and valid values +- [ ] `__init__.py` implements `PluginBase` with `plugin_id` property and `fetch_data()` method +- [ ] `validate_config()` checks all required settings +- [ ] Error handling is comprehensive - `fetch_data()` never raises uncaught exceptions +- [ ] Tests exist in `tests/` with ≥80% coverage +- [ ] All tests pass: `python scripts/run_plugin_tests.py --plugin=my_plugin` +- [ ] Plugin validation passes: `python scripts/validate_plugins.py --verbose` +- [ ] `README.md` documents how the plugin works +- [ ] `docs/SETUP.md` has user-facing setup instructions +- [ ] No hardcoded secrets, API keys, or real personal data +- [ ] Logging uses appropriate levels (`debug`, `info`, `warning`, `error`) + +### Opening the PR + +1. **Push** your feature branch: + + ```bash + git push origin feat-plugin-my-plugin + ``` + +2. **Open a PR** against `main` on GitHub. + +3. **Title**: Use the format `feat: add my_plugin plugin` (or similar descriptive title). + +4. **Description**: Explain what your plugin does, what data source it uses, and mention any required API keys. Reference any related issues. + +5. **Scope**: Keep the PR focused on one plugin. Don't bundle unrelated changes. + +### What CI Checks + +When you open a PR, GitHub Actions automatically: + +1. **Validates manifests** - runs `scripts/validate_plugins.py` to check structure and schema +2. **Runs plugin tests** - discovers your `tests/` directory and runs pytest with coverage +3. **Checks coverage** - verifies your plugin meets the 80% coverage threshold +4. **Verifies required files** - confirms `__init__.py` and `manifest.json` exist +5. **Uploads coverage** - reports to Codecov for review + +All checks must pass before a maintainer can merge your PR. + +### After Merge + +Once merged, add your plugin to the **Available Plugins** list in the repository's main `README.md` (if it isn't already part of your PR). Follow the existing format with emoji, plugin name, and link to your plugin's README. + +--- + +## Best Practices + +### Error Handling + +Always catch exceptions in `fetch_data()` and return a meaningful `PluginResult`: + +```python +def fetch_data(self) -> PluginResult: + try: + # your logic + return PluginResult(available=True, data=data) + except requests.RequestException as e: + logger.warning("Network error: %s", e) + return PluginResult(available=False, error="Network unavailable") + except Exception as e: + logger.exception("Unexpected error in %s", self.plugin_id) + return PluginResult(available=False, error=str(e)) +``` + +### Caching + +`PluginBase` provides **automatic caching** via `get_data()`. When your manifest defines a `refresh_seconds` field in `settings_schema`, the platform caches the result of `fetch_data()` and reuses it until the interval expires. You do **not** need to implement caching yourself in most cases. + +If your plugin needs more control (e.g., different TTLs for different data), you can implement manual caching inside `fetch_data()`: + +```python +from datetime import datetime, timedelta + +class MyPlugin(PluginBase): + def __init__(self, manifest): + super().__init__(manifest) + self._custom_cache = None + self._custom_cache_time = None + self._custom_cache_ttl = timedelta(minutes=5) + + def fetch_data(self) -> PluginResult: + if self._custom_cache and self._custom_cache_time: + if datetime.now() - self._custom_cache_time < self._custom_cache_ttl: + return self._custom_cache + + result = self._fetch_fresh() + if result.available: + self._custom_cache = result + self._custom_cache_time = datetime.now() + return result +``` + +:::tip +For most plugins, just add `refresh_seconds` to your `settings_schema` and let `get_data()` handle caching automatically. Manual caching is only needed for advanced scenarios. +::: + +### Logging + +Use Python's `logging` module with appropriate levels: + +```python +logger.debug("Detailed info for debugging") +logger.info("Plugin initialized successfully") +logger.warning("Non-critical issue: %s", detail) +logger.error("Failed to fetch data: %s", error) +logger.exception("Unexpected error with traceback") +``` + +### Security + +- **Never hardcode** API keys, tokens, or credentials. +- **Never log** secrets - even at debug level. +- Use `"ui:widget": "password"` in `settings_schema` for sensitive fields. +- **Never write your own OAuth flow or store OAuth tokens.** Declare an `oauth` block and call `self.get_oauth_token()`; the platform does the rest. A client secret must never appear in a manifest. See [Signing In with OAuth](/docs/development/plugin-oauth). +- Use generic example data in tests and documentation (e.g., `example@example.com`, well-known public coordinates). + +--- + +## Developing an External Plugin + +You can also develop a plugin as a standalone git repository instead of adding it directly to the FiestaBoard repo. This is the recommended approach for plugins that depend on paid or authenticated APIs. + +### Repository Structure + +Your external plugin repository should have the same structure as a built-in plugin, but at the repository root: + +```text +fiestaboard-plugin--my-weather/ +├── __init__.py # Plugin class (PluginBase subclass) +├── manifest.json # Plugin metadata and configuration schema +├── README.md # Documentation +├── docs/ +│ └── SETUP.md # User-facing setup guide +└── tests/ + └── test_plugin.py # Plugin tests +``` + +### Naming Convention + +If you want your plugin listed in the official **plugin registry**, your repository **must** follow this naming convention: + +```text +fiestaboard-plugin--{name} +``` + +Where `{name}` is lowercase and uses dashes for separators (e.g. `fiestaboard-plugin--my-weather`). The plugin id in `manifest.json` is derived by removing the prefix and converting dashes to underscores (e.g. `my_weather`). + +:::note +This naming convention is only required for the curated registry. When installing a plugin from a custom git URL, any repository name is accepted. +::: + +### Installing Your External Plugin + +During development, install your plugin directly from your git repository. Only HTTPS URLs are accepted (SSH and HTTP URLs are rejected for security): + +```bash +# Install from your repo (HTTPS only) +curl -X POST http://localhost:4420/api/plugins/install \ + -H "Content-Type: application/json" \ + -d '{"repository": "https://github.com/yourname/fiestaboard-plugin--my-weather"}' +``` + +The plugin will be cloned into the `external_plugins/` directory and loaded automatically. + +### Submitting to the Registry + +To get your plugin into the curated registry: + +1. Ensure your repository follows the `fiestaboard-plugin--{name}` naming convention. +2. Open a pull request against the FiestaBoard repository that adds your plugin to `plugin-registry.json`. +3. Your entry should include the plugin id, name, description, repository URL, author, and the date it was added. + +```json +{ + "id": "my_weather", + "name": "My Weather Plugin", + "description": "Custom weather data from my favorite API", + "repository": "https://github.com/yourname/fiestaboard-plugin--my-weather", + "author": "Your Name", + "added": "2026-09-14" +} +``` + +`added` is the date your entry lands, as `YYYY-MM-DD`. The marketplace sorts its **Added** column by it so users can see what is new. + +--- + +## Reference: Plugin API Endpoints + +These REST endpoints are available for testing and debugging your plugin: + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/plugins` | GET | List all loaded plugins | +| `/plugins/{id}` | GET | Get a specific plugin's details | +| `/plugins/{id}/config` | PUT | Update plugin configuration | +| `/plugins/{id}/enable` | POST | Enable a plugin | +| `/plugins/{id}/disable` | POST | Disable a plugin | +| `/plugins/{id}/data` | GET | Fetch current plugin data | +| `/plugins/{id}/variables` | GET | Get the plugin's variable schema | +| `/plugins/registry` | GET | List curated registry plugins | +| `/plugins/registry/{id}/install` | POST | Install a registry plugin | +| `/plugins/install` | POST | Install a plugin from a git URL | +| `/plugins/{id}/uninstall` | DELETE | Uninstall an external plugin | + +--- + +## Reference: Example Plugins + +Study these existing plugins as reference implementations: + +| Plugin | Directory | Highlights | +|--------|-----------|------------| +| **Date & Time** | `plugins/date_time/` | Simple plugin, no external API, `simple` variables only | +| **Guest WiFi** | `plugins/guest_wifi/` | Static data, no API key needed | +| **Weather** | `plugins/weather/` | External API with multiple providers | +| **Stocks** | `plugins/stocks/` | Array variables with indexed items | +| **Muni Transit** | `plugins/muni/` | Nested arrays (stops → lines) | +| **Home Assistant** | `plugins/home_assistant/` | Dynamic entity-based variables | +| **Surf** | `plugins/surf/` | Location-based data, caching pattern | +| **Spotify** | [fiestaboard-plugin--spotify](https://github.com/Fiestaboard/fiestaboard-plugin--spotify) | OAuth sign-in through the platform, rate-limit handling, one request shared by every board | + +--- + +## Next Steps + +- [Signing In with OAuth](/docs/development/plugin-oauth) - Plugins that read a user's account on another service +- [Contributing](/docs/development/contributing) - General contribution guidelines +- [Testing Guide](/docs/development/testing) - Running and writing tests +- [Local Development](/docs/setup/local-development) - Development environment setup diff --git a/versioned_docs/version-9.14/development/plugin-oauth.md b/versioned_docs/version-9.14/development/plugin-oauth.md new file mode 100644 index 0000000000..f41f8a05be --- /dev/null +++ b/versioned_docs/version-9.14/development/plugin-oauth.md @@ -0,0 +1,566 @@ +--- +sidebar_position: 2 +description: "Build a FiestaBoard plugin that signs in to an online account with OAuth: the manifest oauth block, get_oauth_token(), flows, credentials, testing, and a worked Spotify example." +keywords: [FiestaBoard OAuth plugin, get_oauth_token, manifest oauth block, PKCE, device code flow, redirect URI, plugin sign-in, Spotify plugin, plugin development] +--- + +# Signing In with OAuth + +Some data lives behind a user's account on another service: what they're playing, their calendar, their orders. A plugin reaches that data with OAuth, and FiestaBoard does the hard part for you. You **declare** the provider in `manifest.json` and **ask for a token** in `fetch_data`. The platform runs the sign-in, stores the tokens, refreshes them, and draws the sign-in button in your plugin's settings. + +Calling a language model is not a reason to add OAuth: use [FiestaBoard's AI providers](/docs/development/plugin-ai) instead, which the user signs in to once in Settings. + +This page is the complete reference for plugin authors, human or AI. It assumes you have read the [Plugin Development Guide](/docs/development/plugin-guide). For what your users see, read [Connecting Accounts](/docs/features/connecting-accounts). + +**Requires FiestaBoard 9.5.0 or later.** Set `"fiestaboard_version": ">=9.5.0"` in your manifest. A plugin that uses anything marked **9.11.0** on this page (the `key_exchange` and `plex_pin` flows, the [provider quirk fields](#provider-quirks), or `report_oauth_rejected()`) sets `">=9.11.0"` instead. + +## The Short Version + +1. Add an `oauth` block to `manifest.json` naming the provider's endpoints and the scopes you need. +2. In `fetch_data`, call `self.get_oauth_token()`. If it returns `None`, return an unavailable result that tells the user to sign in. Otherwise send it as a bearer token. +3. Never write an OAuth flow, store a token, or ship a client secret. The platform owns all three. + +```json +{ + "id": "example_music", + "name": "Example Music", + "version": "1.0.0", + "fiestaboard_version": ">=9.5.0", + "oauth": { + "provider_name": "Example Music", + "flows": ["relay"], + "authorization_url": "https://example.com/oauth/authorize", + "token_url": "https://example.com/oauth/token", + "scopes": ["user-read-currently-playing"] + }, + "settings_schema": { + "type": "object", + "properties": { + "client_id": { + "type": "string", + "title": "Client ID", + "description": "From the app you created in Example Music's developer settings." + } + } + } +} +``` + +```python +import requests + +from src.plugins.base import PluginBase, PluginResult + +NOT_SIGNED_IN = "Not signed in to Example Music. Open this plugin's settings and sign in." + + +class ExampleMusicPlugin(PluginBase): + @property + def plugin_id(self) -> str: + return "example_music" + + def fetch_data(self) -> PluginResult: + try: + token = self.get_oauth_token() + if not token: + return PluginResult(available=False, error=NOT_SIGNED_IN) + response = requests.get( + "https://api.example.com/v1/me/now-playing", + headers={"Authorization": f"Bearer {token}"}, + timeout=10, + ) + if response.status_code == 401: + return PluginResult(available=False, error="Example Music rejected the sign-in. Press Reconnect.") + response.raise_for_status() + return PluginResult(available=True, data={"title": response.json()["title"].upper()}) + except Exception as exc: # fetch_data must never raise + return PluginResult(available=False, error=str(exc)) + + +Plugin = ExampleMusicPlugin +``` + +That is a working OAuth plugin. The rest of this page explains each choice. + +## Why the Platform Does This + +A FiestaBoard usually lives on a home network at an address like `http://192.168.1.50:4420`. OAuth providers only send a signed-in user back to an `https://` address registered in advance, and a board is neither public nor HTTPS. A plugin cannot solve that by itself, so the platform solves it once for every plugin. Most plugins use one of the first two flows; the other two exist for providers that do not do standard OAuth: + +| Flow | How the user signs in | Use it when | +|------|-----------------------|-------------| +| `relay` | The browser goes to the provider, then returns through a small static page at `https://fiestaboard.app/auth/oauth/redirect`, which hands it back to the board. Authorization code with PKCE. | Always available: every OAuth provider supports it. | +| `device` | The settings show a short code. The user enters it on the provider's site from any device, and the board waits for approval. Device authorization grant (RFC 8628). | The provider supports device codes **for the scopes you need**. No redirect is involved. | +| `key_exchange` | Like `relay`, but with no client ID: the provider trades the code and PKCE verifier for a long-lived API key, sent as JSON. The provider is told where to return with a `callback_url` parameter. **9.11.0.** | OpenRouter-style "sign in to get a key" providers. | +| `plex_pin` | The board creates a PIN at plex.tv and opens Plex's sign-in page in a new tab. The settings wait while the board polls the PIN for a token. **9.11.0.** | Plex only. The block needs no endpoints: they are plex.tv's. | + +If the browser does not make it back to the board after a `relay` or `key_exchange` sign-in, the user can paste the address they landed on (or the code the provider showed) into the settings, and the board finishes the sign-in from that. You do nothing to enable it. + +The redirect URI for the `relay` flow is the same for every board and every plugin: + +```text +https://fiestaboard.app/auth/oauth/redirect +``` + +FiestaBoard 9.5.0 through 9.7.x sends the same address with `.html` on the end. Both reach the same page, but providers compare redirect URIs exactly. So in your setup guide, tell users to **copy the redirect URI from the plugin's settings**, which always shows the one their board sends, instead of typing one from your docs. + +The first time someone signs in from a browser, that page shows the board's address and asks them to confirm it, then remembers the answer. It only ever forwards to an address on a local network. You do not build, host, or configure any of it. + +## The `oauth` Block + +| Field | Required | Meaning | +|-------|----------|---------| +| `flows` | yes | `["relay"]`, `["device"]`, or both. The first one listed is the one the sign-in button uses. `key_exchange` and `plex_pin` are also accepted from 9.11.0. | +| `token_url` | yes, except for `plex_pin` | The provider's token endpoint. For `key_exchange`, the endpoint that trades the code for a key. | +| `authorization_url` | for `relay` and `key_exchange` | The provider's authorization endpoint. | +| `device_authorization_url` | for `device` | The provider's device authorization endpoint. | +| `scopes` | no | Scopes to request. Ask for the least that works; users see the list on the consent screen. | +| `provider_name` | no | Shown in the UI ("Sign in with Example Music"). Defaults to your plugin's name. | +| `client_id` | no | A client ID shipped with the plugin. See [Whose App?](#whose-app). | +| `client_id_setting` | no | The `settings_schema` key that holds a user's own client ID. Default `client_id`. | +| `client_secret_setting` | no | The `settings_schema` key that holds a user's client secret, for providers that require one. Omit it for a PKCE-only client. | +| `authorization_params` | no | Extra fixed query parameters for the authorization request, as strings. For example `{"access_type": "offline"}`. | +| `app_setup_url` | no | The provider's developer page, where a user creates their own app. The guided setup links to it. `https://` only. Added in 9.8.0; see the note on unknown fields below. | +| `client_id_param` | no | The name the provider gives the client ID parameter, in the authorization request and the token request. Default `client_id`; TikTok wants `client_key`. **9.11.0.** | +| `scope_separator` | no | How scopes are joined: `" "` (the default) or `","`. Strava, TikTok, and Todoist want `","`. **9.11.0.** | +| `device_scope_param` | no | The scope parameter's name on the device authorization request. Default `scope`; Twitch wants `scopes`. **9.11.0.** | +| `device_poll_scope` | no | `true` to send the scopes again when polling the device token endpoint (Twitch). Default `false`. **9.11.0.** | +| `endpoint_base_setting` | no | A `settings_schema` key that holds the provider's base address, for a provider that runs on the user's own network (Home Assistant). The endpoint fields then hold paths, such as `"/auth/token"`. **9.11.0.** | +| `plex_product` | no | The product name the `plex_pin` flow sends to Plex, shown on Plex's sign-in page and device list. Default `FiestaBoard`. **9.11.0.** | +| `token_auth_method` | no | How the client secret reaches the token endpoint: `"post"` (the default, in the form body) or `"basic"` (an HTTP Basic `Authorization` header, `client_secret_basic`). Applies to the code exchange, refresh, and device poll. Without a secret the client ID stays in the body either way. X wants `"basic"` for confidential apps. **9.11.0.** | +| `refresh_params` | no | Extra form fields sent with every refresh, as strings. For example `{"scope": "offline"}` (WHOOP). May not set `grant_type`, `refresh_token`, `client_id`, `client_secret`, or the name in `client_id_param`. **9.11.0.** | + +The manifest is validated when the plugin loads, and a plugin with a bad `oauth` block is refused with a message naming the problem: + +- Endpoints must be `https://`. Plain `http://` is accepted only for `localhost`, `127.0.0.1`, and `::1`, which is what a local test provider looks like. With `endpoint_base_setting`, the endpoints must be paths starting with `/` instead, and the address the user saves is checked when they sign in (see [Provider Quirks](#provider-quirks)). +- `flows` must be a non-empty list of `relay`, `device`, `key_exchange`, and `plex_pin`, without repeats. +- `client_id_param`, `device_scope_param`, and `endpoint_base_setting` are parameter names: letters, digits, and underscores. `scope_separator` is `" "` or `","`. `device_poll_scope` is `true` or `false`. `token_auth_method` is `"post"` or `"basic"`. `refresh_params` is an object of strings. +- `endpoint_base_setting` must name a key in `settings_schema`. +- **`client_secret` is never allowed in a manifest.** Plugin repositories are public. +- A field your board's FiestaBoard does not know is ignored and reported as a warning in `GET /plugins/errors`, so a plugin using a field from a newer release still loads on an older one. **9.5.0 through 9.7.x refuse the whole plugin instead**, so a plugin that must run on those releases cannot use `app_setup_url`. A 9.8.x board ignores the 9.11.0 fields the same way, which means it would sign in with the wrong parameter names: set `fiestaboard_version` to `>=9.11.0` when you use them. A flow name an older board does not know refuses the plugin there. +- `authorization_params` may not set `response_type`, `client_id`, `redirect_uri`, `state`, `scope`, `code_challenge`, or `code_challenge_method`, nor the name in `client_id_param`. The platform sets those. +- `scopes` entries are strings without spaces. +- Transition plugins may not declare `oauth`. + +## Provider Quirks {#provider-quirks} + +**Requires FiestaBoard 9.11.0.** Many providers bend the OAuth standard a little. These fields cover the common bends, so a plugin never has to run its own flow. Each one has a default that matches the standard, so leave out any field you do not need. + +A provider that calls the client ID something else and joins scopes with commas (TikTok): + +```json +"oauth": { + "provider_name": "TikTok", + "flows": ["relay"], + "authorization_url": "https://www.tiktok.com/v2/auth/authorize/", + "token_url": "https://open.tiktokapis.com/v2/oauth/token/", + "scopes": ["user.info.basic", "user.info.stats"], + "client_id_param": "client_key", + "scope_separator": "," +} +``` + +A device flow with a differently named scope parameter, where the token poll repeats the scopes (Twitch): + +```json +"oauth": { + "provider_name": "Twitch", + "flows": ["device"], + "device_authorization_url": "https://id.twitch.tv/oauth2/device", + "token_url": "https://id.twitch.tv/oauth2/token", + "scopes": ["user:read:follows"], + "device_scope_param": "scopes", + "device_poll_scope": true +} +``` + +A provider that runs on the user's own network (Home Assistant). The endpoints are paths, and the base address comes from a setting the user fills in: + +```json +"oauth": { + "provider_name": "Home Assistant", + "flows": ["relay"], + "endpoint_base_setting": "base_url", + "authorization_url": "/auth/authorize", + "token_url": "/auth/token" +}, +"settings_schema": { + "type": "object", + "properties": { + "base_url": {"type": "string", "title": "Home Assistant address"} + } +} +``` + +When the user signs in, the board joins the saved address to each path. It sends credentials only to an `https://` address, or to plain `http://` on the user's own network: a private IP address, `localhost`, a single-label name, or a name ending in `.local`, `.lan`, `.home.arpa`, or `.docker.internal`. Link-local addresses (`169.254.x.x`), other `.internal` names, and cloud metadata hosts are refused. Anything else, an empty value, or an address with a query, fragment, or user name stops the sign-in with a message asking the user to check the address. + +A Plex sign-in. The block has no endpoints and no client ID: + +```json +"oauth": { + "provider_name": "Plex", + "flows": ["plex_pin"], + "plex_product": "FiestaBoard" +} +``` + +`get_oauth_token()` returns the Plex token. Send it the way Plex expects, as the `X-Plex-Token` header, not as a bearer token. Each board has its own `X-Plex-Client-Identifier`, created on first use and kept in `data/.oauth_client_identifier`. + +A provider that hands out an API key instead of tokens (OpenRouter's flow): + +```json +"oauth": { + "provider_name": "Example Router", + "flows": ["key_exchange"], + "authorization_url": "https://router.example.com/auth", + "token_url": "https://router.example.com/api/v1/auth/keys" +} +``` + +The key does not expire, so there is nothing to refresh. The settings also offer **Sign in without a browser redirect**, which asks the provider to show a code that the user pastes back. + +A confidential client that must send its secret as HTTP Basic, and a provider that wants a field on every refresh: + +```json +"oauth": { + "provider_name": "X", + "flows": ["relay"], + "authorization_url": "https://x.com/i/oauth2/authorize", + "token_url": "https://api.x.com/2/oauth2/token", + "scopes": ["users.read", "tweet.read", "offline.access"], + "client_secret_setting": "client_secret", + "token_auth_method": "basic" +} +``` + +```json +"refresh_params": {"scope": "offline"} +``` + +The platform also reads a few answer shapes without any field: Twitch's `{"status": 400, "message": "authorization_pending"}` poll errors (and `slow_down`, `invalid device code`, `Invalid refresh token`), a `scope` sent as a JSON array, and a token answer wrapped as `{"data": [{...}]}` (Instagram). + +### Swapping and renewing the token yourself {#token-hooks} + +**Requires FiestaBoard 9.11.0.** Some providers hand out a short-lived token at sign-in that the app is expected to trade for a long-lived one, and renew with a call of their own rather than a refresh token (Meta: Instagram, Threads, Facebook). Two optional `PluginBase` methods cover that. The platform still stores the token, so it survives a restart: + +```python +def exchange_oauth_token(self, token): + """Called once after each sign-in. Return None to keep the token.""" + long_lived = self._exchange_for_long_lived(token["access_token"]) # your HTTP call + return {"access_token": long_lived["access_token"], "expires_in": long_lived["expires_in"]} + +def refresh_oauth_token(self, token): + """Called when the stored token is within a minute of expires_at. Return None for the standard refresh.""" + renewed = self._renew(token["access_token"]) + return {"access_token": renewed["access_token"], "expires_in": renewed["expires_in"]} +``` + +- Both receive `{"access_token", "refresh_token", "expires_at", "scopes"}` (`expires_at` is epoch seconds or `None`) and return `None` or `{"access_token", "expires_in"?, "refresh_token"?}`. A missing `refresh_token` keeps the one already stored; a missing `expires_in` means the token does not expire. +- If `exchange_oauth_token` raises or returns something without an `access_token`, the sign-in token is kept. If `refresh_oauth_token` raises, the current token is served until it expires and the hook is asked again on the next fetch. +- To renew earlier than the provider's expiry, return a shorter `expires_in`. +- Neither hook may call `get_oauth_token()` or `report_oauth_rejected()`. +- The settings your hooks need, such as a client secret, come from `self.config` as usual. A board before 9.11.0 never calls them. + +## Whose App? {#whose-app} + +Every OAuth sign-in runs under an "app" (or "client") registered with the provider. You choose who registers it. The choice decides what the user sees. + +### Each user registers their own app + +Declare a `client_id` field in `settings_schema` and do not ship a `client_id` in the `oauth` block. The user creates an app in the provider's developer settings, gives it the redirect URI, and pastes the app's client ID into your plugin's settings. + +FiestaBoard turns this into a guided setup in the plugin's **Account connection** section: numbered steps, a link to the provider's developer page (set `app_setup_url`), a link to your `docs/SETUP.md`, the redirect URI with a copy button, and the Client ID field itself. The sign-in button saves what was typed and starts the sign-in in one press. Your client ID field (and client secret field, if any) appears there and not in the general settings form, so declare it plainly and do not mark it `required`: the panel already will not sign in without it. + +This works for every user without limits, and it is the right default: most providers restrict apps they have not reviewed. Your `docs/SETUP.md` must still walk through creating the app, with what to enter in each of the provider's fields. + +If the provider requires a client secret, add `"client_secret_setting": "client_secret"` and a matching password field: + +```json +"client_secret": { + "type": "string", + "title": "Client Secret", + "ui:widget": "password" +} +``` + +The names `client_id` and `client_secret` are masked in API responses automatically. The secret is stored on the user's board and sent only to the provider's token endpoint. + +### The plugin brings its own app + +Ship your app's client ID in the `oauth` block and offer **no** client ID field: + +```json +"oauth": { + "provider_name": "Spotify", + "flows": ["relay"], + "authorization_url": "https://accounts.spotify.com/authorize", + "token_url": "https://accounts.spotify.com/api/token", + "scopes": ["user-read-playback-state"], + "client_id": "0123456789abcdef0123456789abcdef" +} +``` + +Users open the settings and see one button, **Sign in with Spotify**, and nothing to set up. A client ID is not a secret, so publishing it is fine. This only works with providers that support PKCE without a client secret, because a secret cannot ship. + +Before you choose this, check the provider's limits on apps they have not reviewed. Spotify, for example, admits only five hand-added accounts to an app in Development Mode and offers no wider mode to open-source projects. Every other user signs in successfully and then gets `403` from the API. If your provider has a limit like this, say so at the top of your setup guide and make your `403` message explain it. + +### The rule that decides + +**A client ID or secret saved in the plugin's config only counts if `settings_schema` declares that field.** + +| Manifest | Client ID used | +|----------|----------------| +| `oauth.client_id` shipped, no settings field | Always the shipped one. A value saved earlier or through the API is ignored. | +| `oauth.client_id` shipped, settings field declared | The user's value if they saved one, otherwise the shipped one. | +| No `oauth.client_id`, settings field declared | The user's value. The sign-in button is disabled until one is saved. | + +On FiestaBoard 9.5.0 through 9.7.x a saved value always overrode a shipped client ID, and the settings showed app-setup help even when there was nothing to set up. 9.8.0 and later follow the table above. + +## Using the Token + +`self.get_oauth_token()` returns the current access token as a string, or `None`. + +- **Call it on every fetch and use what it returns.** Do not keep the token in an attribute, a cache, or a file. The platform refreshes it shortly before it expires, and the next call returns the new one. +- **`None` means the user is not signed in, or has to sign in again.** You cannot tell the two apart, so word the message to cover both: "Open this plugin's settings and sign in." Return `PluginResult(available=False, error=...)` and make no request. +- **Each plugin instance has its own connection.** Two instances of your plugin can be signed in to two accounts. Another plugin never sees your tokens. + +### Handling the provider's answers + +The platform manages the token. Your plugin still has to handle what the provider's API says: + +| Answer | What it means | What to do | +|--------|---------------|------------| +| `401` | The token was rejected, usually because the user revoked access. | Return unavailable and tell the user to press **Reconnect**. Do not retry on every render. | +| `403` | Signed in, but not allowed. Common with apps the provider limits to listed accounts. | Return unavailable with a message that says what to do about it. | +| `429` | Rate limited. | Honor `Retry-After`. Do not call again until it has passed. | +| `5xx`, timeout | The provider is having trouble. | Back off for a short while. Consider showing the last good data briefly. | + +Two platform behaviors make this your job rather than something you get for free: + +- **Unavailable results are not cached.** After a failure, `fetch_data` is called again on the next render. Without your own cooldown, a rate-limited plugin keeps hitting the provider. +- **Results are cached per board shape.** A Flagship and a Note showing the same plugin fetch separately. For a rate-limited API, keep one shared snapshot inside the plugin for a few seconds so that both are served by one request. + +### Reporting a rejected token + +**Requires FiestaBoard 9.11.0.** When the provider answers `401`, call `self.report_oauth_rejected()`. The platform refreshes the token once if it can and returns the new one, so retry the request once with it. `None` means the user has to sign in again. The settings then say *Provider* stopped accepting the sign-in, so return an unavailable result and make no more requests. + +```python +if response.status_code == 401: + report = getattr(self, "report_oauth_rejected", None) # absent before 9.11.0 + new_token = report() if report else None + if not new_token: + return PluginResult(available=False, error="Example Music rejected the sign-in. Sign in again in this plugin's settings.") + response = requests.get(url, headers={"Authorization": f"Bearer {new_token}"}, timeout=10) +``` + +If several requests were in flight with the same token, each may report its own `401`: the platform knows which token it last gave the plugin, so a report about a token it has already replaced just returns the new one. A plugin that keeps tokens elsewhere can pass the refused one as `report_oauth_rejected(token=...)`. + +The forced refresh runs at most once a minute per connection, so a plugin that reports on every render does not hammer the provider. Report only a real rejection of the token, never a `403`, `429`, or outage. + +On 9.5.0 through 9.8.x, or if you do not call it, the settings can keep showing **Connected** until the platform's next refresh fails. Your error message is what the user sees in the meantime, so make it actionable. + +### Guarding against an older FiestaBoard + +Setting `fiestaboard_version` to `>=9.5.0` is the main protection. If you want a friendly message on an older board that somehow loads the plugin: + +```python +get_token = getattr(self, "get_oauth_token", None) +if get_token is None: + return PluginResult(available=False, error="Update FiestaBoard to use this plugin.") +``` + +## What Your Users See + +You build no UI. When a plugin declares `oauth`, its settings gain an **Account connection** section: + +- **Not connected**: a **Sign in with *Provider*** button. With the user's-own-app model it sits under the guided setup described in [Whose App?](#whose-app) and stays disabled until a Client ID is entered. +- **Waiting for approval** (device flow): the address to visit and the code to enter, with a copy button. It updates by itself when the user approves. +- **Waiting for Plex** (`plex_pin`): Plex's sign-in page opens in a new tab, and the settings update by themselves when the user approves. There is no code to enter. +- **Paste to finish** (9.11.0): after a `relay` or `key_exchange` start, a **Sign-in didn't come back? Paste the address or code** box is offered for ten minutes. The user pastes the address the browser stopped at, or the code the provider showed, and presses **Finish sign-in**. +- **Connected**: **Reconnect** and **Disconnect** buttons. +- **Reconnect needed**: the provider refused a token refresh. Your plugin gets `None` until the user signs in again. When your plugin reported the rejection (9.11.0), the message reads "*Provider* stopped accepting the sign-in. Sign in again." + +After a relay sign-in the user lands back on the Integrations page with your plugin's settings open and a confirmation. Failures arrive there too, as a sentence. + +## Testing + +### Unit tests + +Do not test the OAuth flow; the platform does. Test what your plugin does with a token and without one. Replace `get_oauth_token` on the instance and mock the provider's API as you would for any HTTP plugin: + +```python +import json +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from plugins.example_music import ExampleMusicPlugin + +MANIFEST = json.loads((Path(__file__).parent.parent / "manifest.json").read_text()) + + +@pytest.fixture +def plugin(): + p = ExampleMusicPlugin(MANIFEST) + p.get_oauth_token = lambda: "test_access_token" + return p + + +def test_sends_the_token_as_a_bearer_header(plugin): + with patch("plugins.example_music.requests.get") as get: + get.return_value = Mock(status_code=200, json=lambda: {"title": "Low Tide"}) + result = plugin.fetch_data() + assert result.available is True + assert get.call_args.kwargs["headers"]["Authorization"] == "Bearer test_access_token" + + +def test_makes_no_request_when_not_signed_in(plugin): + plugin.get_oauth_token = lambda: None + with patch("plugins.example_music.requests.get") as get: + result = plugin.fetch_data() + assert result.available is False + assert "sign in" in result.error.lower() + get.assert_not_called() +``` + +Also cover `401`, `403`, `429` with `Retry-After`, a timeout, and that a new token is used immediately after a rejection. + +Validate your `oauth` block with the platform's own validator, so that a mistake fails in your CI and not at install time: + +```python +from src.oauth.provider import parse_provider_block, validate_provider_block + + +def test_oauth_block_is_valid_and_has_no_secret(): + block = MANIFEST["oauth"] + assert validate_provider_block(block) == [] + assert "client_secret" not in block + assert parse_provider_block(block, MANIFEST["name"]).flows == ("relay",) +``` + +Use invented account data in fixtures, and obviously fake tokens such as `test_access_token`. + +### CI + +A plugin repository's CI checks out FiestaBoard to run against. Pin that checkout to a release that has OAuth, and keep it in step with `fiestaboard_version`: + +```yaml +- uses: actions/checkout@v4 + with: + repository: Fiestaboard/FiestaBoard + ref: v9.5.0 # keep equal to fiestaboard_version in manifest.json + path: fiestaboard-core +``` + +### End to end, without a real account + +FiestaBoard ships a strict stand-in provider, `scripts/mock_oauth_provider.py`. It requires PKCE, issues single-use codes, rotates refresh tokens, and expires access tokens after 20 seconds so that refresh happens while you watch. It accepts the client secret in the body or as HTTP Basic, and also serves the other flows: `key_exchange` (`/auth` and `/api/v1/auth/keys`), Plex PINs (`/api/v2/pins` and an approval page at `/plex/auth`), the authorize and token pair under Home Assistant's `/auth/…` and Hugging Face's `/oauth/…` paths, and an OpenAI-style `/v1/responses` stream with `/v1/models`. The docstring at the top of the script lists every endpoint. + +1. Start the development stack with port 9400 published, because the relay flow sends your browser to the provider. Save this next to `docker-compose.dev.yml` as `docker-compose.oauth-test.yml`: + + ```yaml + services: + fiestaboard: + ports: + - "9400:9400" + ``` + + ```bash + docker compose -f docker-compose.dev.yml -f docker-compose.oauth-test.yml up -d + docker compose -f docker-compose.dev.yml exec -d fiestaboard python scripts/mock_oauth_provider.py + ``` + +2. Put a **copy** of your plugin in `data/external_plugins//` and, in that copy only, point the endpoints at the mock: + + ```json + "authorization_url": "http://localhost:9400/authorize", + "device_authorization_url": "http://localhost:9400/device/code", + "token_url": "http://localhost:9400/token" + ``` + + Point the copy's API calls at `http://localhost:9400/me` too, or stub them. Restart the container so the plugin loads. + +3. Open `http://localhost:4420`, go to **Integrations**, open the plugin's settings, and sign in. The relay flow passes through the real `fiestaboard.app` page and back. For the device flow, approve the code as if from another device: + + ```bash + curl "http://localhost:9400/device/approve?user_code=" + ``` + +4. Check the cases that matter. A plugin's results are cached for its refresh interval (five minutes unless it declares `refresh_seconds`), so to make the plugin fetch again straight away, turn it off and on with its toggle on the Integrations page. + - Wait 20 seconds and make it fetch: the board refreshes the token and your plugin keeps working. + - `curl -X POST http://localhost:9400/revoke-all`, then make it fetch: the settings show **Reconnect needed** and your plugin reports that it is not signed in. + - Press **Disconnect**: your plugin reports that it is not signed in on its next fetch. Signing in and disconnecting clear the plugin's cache, so these two take effect without the toggle. + - `curl http://localhost:9400/log` shows every request the provider saw, with secrets replaced by their lengths. + +For endpoints that are not in a manifest (Plex, and the FiestaBot AI sign-ins), set `FIESTABOARD_OAUTH_URL_OVERRIDES` on the container to a JSON object of URL prefixes and their replacements, for example `{"https://plex.tv": "http://localhost:9400", "https://app.plex.tv": "http://localhost:9400/plex", "https://openrouter.ai": "http://localhost:9400"}`. Approve a Plex PIN without a browser with `curl "http://localhost:9400/plex/approve?code="`. This variable is for development only; leave it unset on a real board. + +Never commit the copy that points at the mock. Finish with one sign-in against the real provider before you publish. + +## Documenting It for Your Users + +Your `docs/SETUP.md` should cover, in this order: + +1. **What the plugin can see.** List the scopes in plain words and say what it cannot do ("It can't play, pause, or change anything"). +2. **Creating the app**, if users register their own: where to go, which fields matter, and that the redirect URI is copied from the plugin's settings. Mention any plan or account requirement the provider has. +3. **Signing in**: open the settings, press **Sign in with *Provider***, approve, and confirm the board's address the first time. Link to [Connecting Accounts](/docs/features/connecting-accounts) for the details. +4. **Limits**, such as a cap on accounts, stated before the user hits them. +5. **Troubleshooting** keyed by the exact error text your plugin shows. + +## Provider Notes + +Check the provider's current documentation; these are starting points, not guarantees. + +| Provider | Flow | Notes | +|----------|------|-------| +| Spotify | `relay` | PKCE without a secret. No device flow. Development Mode apps admit five listed accounts, and the redirect URI must match exactly. | +| Google | `relay` | Web-application clients need a client secret, so use `client_secret_setting`. Send `{"access_type": "offline", "prompt": "consent"}` in `authorization_params` to receive a refresh token. The device flow allows only a short list of scopes. | +| GitHub | `device` or `relay` | The device flow must be enabled in the app's settings. GitHub reports OAuth errors with HTTP 200 and separates scopes with commas; the platform handles both. | +| Strava | `relay` | `"scope_separator": ","`. | +| TikTok | `relay` | `"client_id_param": "client_key"` and `"scope_separator": ","`. | +| Todoist | `relay` | `"scope_separator": ","`. | +| Twitch | `device` | `"device_scope_param": "scopes"` and `"device_poll_scope": true`. The platform reads Twitch's `message` errors and array scopes. | +| X | `relay` | Confidential (Web App) clients: `"token_auth_method": "basic"`. Native App clients have no secret and need nothing extra. | +| WHOOP | `relay` | `"refresh_params": {"scope": "offline"}`. | +| Instagram, Threads, Facebook | `relay` | Trade the 1-hour token for a long-lived one in `exchange_oauth_token` and renew it in `refresh_oauth_token`. See [Swapping and renewing the token yourself](#token-hooks). | +| Home Assistant | `relay` | Runs on the user's network: `endpoint_base_setting` with path endpoints. | +| Plex | `plex_pin` | Not OAuth. Send the token as `X-Plex-Token`. | +| OpenRouter | `key_exchange` | Yields an API key with no client ID and no expiry. | + +## What the Platform Guarantees + +- **PKCE on every relay sign-in.** The code verifier is created on the board and never leaves it, so an authorization code is useless to anything that sees it in transit. +- **A signed, single-use, ten-minute `state`.** A callback that this board did not start, that has expired, or that was already used is refused. +- **Tokens stay on the board.** They are stored in `data/oauth_tokens.json`, readable only by the FiestaBoard process, never included in an API response, and never written to backups. +- **One connection per plugin instance.** Tokens are not shared between plugins. +- **Cleanup.** Uninstalling a plugin, deleting an instance, or pressing **Disconnect** deletes the tokens. Disconnecting and signing in also clear the plugin's cached results, so the board stops showing an account's data as soon as the user disconnects it. +- **Refresh.** Access tokens are refreshed a minute before they expire. If the provider refuses, the connection becomes **Reconnect needed** and is not retried on every fetch. + +## Checklist + +- [ ] `fiestaboard_version` is `>=9.5.0`, or `>=9.11.0` if the plugin uses a 9.11.0 flow, field, or `report_oauth_rejected()` +- [ ] The `oauth` block passes `validate_provider_block` in a test (pass `settings_schema` too when you use `endpoint_base_setting`) +- [ ] No client secret anywhere in the repository +- [ ] Scopes are the least the plugin needs +- [ ] `fetch_data` calls `get_oauth_token()` every time and stores nothing +- [ ] `None` returns an unavailable result that says to sign in, and makes no request +- [ ] `401`, `403`, `429`, `5xx`, and timeouts are handled with a cooldown +- [ ] One request serves every board shape, if the API is rate limited +- [ ] Tests use invented data and fake tokens +- [ ] CI is pinned to a FiestaBoard release with OAuth +- [ ] `docs/SETUP.md` covers permissions, app creation (if any), sign-in, limits, and troubleshooting +- [ ] One sign-in has been tested against the real provider + +## Worked Example: Spotify + +The [Spotify plugin](https://github.com/Fiestaboard/fiestaboard-plugin--spotify) is the reference implementation. It is worth reading in full: + +- `manifest.json`: the `relay` flow, three read-only scopes, and a `client_id` setting, because Spotify limits a shared app to five accounts and so each user brings their own. +- `__init__.py`: one shared snapshot for every board shape, cooldowns for `401`, `403`, `429`, and outages, and stale data shown briefly during an outage. +- `tests/`: `get_oauth_token` replaced on the instance, a fake Spotify API, and the platform's validator run against the manifest. +- `docs/SETUP.md`: a setup guide that walks through creating the Spotify app, field by field. + +## Next Steps + +- [Plugin Development Guide](/docs/development/plugin-guide) - Everything else about building a plugin +- [Connecting Accounts](/docs/features/connecting-accounts) - The sign-in from the user's side +- [Testing Guide](/docs/development/testing) - Running and writing tests diff --git a/versioned_docs/version-9.14/development/testing.md b/versioned_docs/version-9.14/development/testing.md new file mode 100644 index 0000000000..0271e3b928 --- /dev/null +++ b/versioned_docs/version-9.14/development/testing.md @@ -0,0 +1,161 @@ +--- +sidebar_position: 5 +description: "Run and write tests for FiestaBoard using pytest for Python and Vitest for TypeScript/React components." +keywords: [FiestaBoard testing, pytest, Vitest, unit tests, integration tests, test guide, Python tests, TypeScript tests] +--- + +# Testing Guide + +How to run and write tests for FiestaBoard. + +## Running Tests + +### Python Backend Tests + +```bash +# Run all Python tests +pytest tests/ + +# Run with coverage report +pytest tests/ --cov=src --cov=plugins --cov-report=term-missing + +# Run specific test file +pytest tests/test_api_server.py + +# Run specific test +pytest tests/test_api_server.py::test_health_endpoint +``` + +### Web UI Tests + +```bash +# Run all web tests +npm run test:web + +# Run with coverage +cd web && npx vitest --coverage + +# Run specific test file +cd web && npx vitest src/components/__tests__/PageEditor.test.tsx +``` + +### Linting + +```bash +# Lint web code (ESLint) +npm run lint:web + +# Lint Python code (Pylint) +pylint src/ plugins/ +``` + +## Writing Tests + +### Python Plugin Tests + +Plugins require a minimum of **80% test coverage**. Use the provided test utilities: + +`PluginBase` takes a **manifest** dict in its constructor, not a `config` kwarg. +Set config separately via the `plugin.config` attribute. `validate_config(config)` +takes the config dict as an argument and returns a **list of error strings** +(empty when valid) — it never returns a bool. + +```python +from unittest.mock import patch, MagicMock + +class TestMyPlugin: + """Tests for MyPlugin.""" + + def _make_plugin(self, config=None): + """Create a plugin instance with a minimal manifest.""" + plugin = MyPlugin({"id": "my_plugin", "name": "My Plugin", "version": "1.0.0"}) + if config: + plugin.config = config + return plugin + + def test_fetch_data_success(self): + """Test successful data fetching.""" + plugin = self._make_plugin({"api_key": "test"}) + + with patch("requests.get") as mock_get: + mock_get.return_value = MagicMock( + status_code=200, + json=lambda: {"temp": 72} + ) + result = plugin.fetch_data() + + assert result.available is True + assert result.data["temp"] == 72 + + def test_fetch_data_api_error(self): + """Test handling of API errors.""" + plugin = self._make_plugin({"api_key": "test"}) + + with patch("requests.get") as mock_get: + mock_get.side_effect = Exception("API Error") + result = plugin.fetch_data() + + assert result.available is False + assert result.error is not None + + def test_validate_config(self): + """Test configuration validation.""" + plugin = self._make_plugin() + + # Missing required key returns a non-empty list of errors + assert plugin.validate_config({}) != [] + + # Valid config returns an empty list + assert plugin.validate_config({"api_key": "test"}) == [] +``` + +### Web UI Component Tests + +Use Vitest with React Testing Library: + +```typescript +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import MyComponent from './MyComponent'; + +describe('MyComponent', () => { + it('renders correctly', () => { + render(); + expect(screen.getByText('Test')).toBeDefined(); + }); + + it('handles click events', async () => { + const onClick = vi.fn(); + render(); + + await screen.getByRole('button').click(); + expect(onClick).toHaveBeenCalled(); + }); +}); +``` + +## Test Best Practices + +1. **Mock external APIs** - Never make real API calls in tests +2. **Test happy path and error cases** - Cover both success and failure scenarios +3. **Test edge cases** - Empty data, missing config, rate limits +4. **Keep tests focused** - One assertion per test when possible +5. **Use descriptive names** - Test names should explain what's being tested + +## CI/CD Integration + +Tests run automatically on every pull request via GitHub Actions. The CI pipeline: + +1. Runs Python tests with coverage +2. Runs web UI tests with coverage +3. Runs linting checks +4. Reports coverage to the PR + +:::info +All tests must pass before a PR can be merged. Coverage must meet the minimum threshold for plugins (80%). +::: + +## Next Steps + +- [Contributing](/docs/development/contributing) - How to contribute +- [Plugin Development Guide](/docs/development/plugin-guide) - Creating plugins with tests diff --git a/versioned_docs/version-9.14/features/_category_.json b/versioned_docs/version-9.14/features/_category_.json new file mode 100644 index 0000000000..647db45de7 --- /dev/null +++ b/versioned_docs/version-9.14/features/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Features", + "position": 3, + "link": { + "type": "generated-index", + "description": "Learn about FiestaBoard's key features." + } +} diff --git a/versioned_docs/version-9.14/features/connecting-accounts.md b/versioned_docs/version-9.14/features/connecting-accounts.md new file mode 100644 index 0000000000..a8afa53e1a --- /dev/null +++ b/versioned_docs/version-9.14/features/connecting-accounts.md @@ -0,0 +1,126 @@ +--- +sidebar_position: 8 +description: "Connect a FiestaBoard plugin to an online account such as a music or calendar service, even though your board is only reachable on your home network." +keywords: [FiestaBoard OAuth, connect account, sign in, plugin sign-in, redirect URI, device code, self-hosted OAuth] +--- + +# Connecting Accounts + +Some plugins show data from an account you own on another service: what is playing, your next event, your latest order. Those plugins ask you to **connect** the account once. You sign in on the service's own page; FiestaBoard never sees your password. + +A plugin that needs this has an **Account connection** section at the top of its settings. + +FiestaBot can sign in to some AI providers the same way, instead of using an API key. That happens in **Settings → AI Providers**; see [AI Providers](/docs/setup/ai-providers#signing-in-instead-of-using-an-api-key). Everything on this page about pasting an address applies there too. + +## Connecting + +1. Open **Integrations**, find the plugin, and open its settings. +2. Some plugins bring their own app and need nothing else. Others ask for a **client ID** (and sometimes a secret): enter it and press **Save Changes**. The plugin's own setup guide says where to get one. See [Creating your own app](#creating-your-own-app) below. +3. Press **Sign in with** and the service's name, such as **Sign in with Spotify**. + +What happens next depends on the plugin. + +### Plugins that send you to the service + +You are taken to the service to sign in and approve access, then brought back to your board. + +On the way back you pass through `fiestaboard.app`. **The first time you connect from a browser, it stops and shows your board's address**, like `http://192.168.1.50:4420`, and asks you to confirm: + +- Check that the address is the one you use to open FiestaBoard. +- Leave **Remember this board** ticked so you are not asked again in that browser. +- Press **Continue to my board**. + +You land back on Integrations with the plugin connected. + +:::info Why the extra stop? +Online services will only send you back to a secure, public web address they were told about in advance. Your board is neither: it lives on your home network. So every FiestaBoard uses the same public return address, and that page hands you on to your own board. It runs entirely in your browser, stores nothing on a server, and will only ever hand a sign-in to an address on a local network. It asks before using an address it has not seen so that a link from someone else cannot quietly send your sign-in to a different machine. +::: + +### Plugins that show you a code + +Some services let you approve from another device instead. The plugin's settings show a web address and a short code: + +1. Open the address on your phone or computer. +2. Enter the code and approve. + +The settings update by themselves within a few seconds. If the code expires before you approve it, press **Sign in with** again for a new one. While a code is still showing, **Get a new code** replaces it. + +### Plex + +**Sign in with Plex** opens Plex's sign-in page in a new tab. Approve FiestaBoard there. The settings say they are waiting and update by themselves when you approve; there is no code to type. + +### When the sign-in doesn't come back + +Sometimes the browser does not make it back to your board: you approved on a different device, the return page could not reach the board, or the service shows a code instead of sending you back. After you press **Sign in with**, the settings offer **Sign-in didn't come back? Paste the address or code** for ten minutes: + +1. Open it. +2. Paste the whole address from the tab where the sign-in stopped, or the code the service showed you. +3. Press **Finish sign-in**. + +**Open the sign-in page again** reopens the service's page if you closed it. Some services always end this way; for those the box opens by itself and says what to copy. + +Some services also offer **Sign in without a browser redirect**. The service then shows a code, and you paste it into the same box. + +## Creating your own app + +Only needed for plugins whose settings ask for a client ID. If the settings show just a sign-in button, the plugin brings its own app; skip this section. + +Most services require an "app" or "client" registration before they will let anything sign in on your behalf. It is free and takes a few minutes in the service's developer settings. The plugin's setup guide links to the right page. + +The plugin's **Account connection** section walks you through it in numbered steps: + +1. **Create the app.** The section links to the service's developer page and to the plugin's step-by-step guide. +2. **Give the app the redirect URI** (also called a callback URL). Copy it from the section with the copy button. It looks like this: + + ```text + https://fiestaboard.app/auth/oauth/redirect + ``` + + Services compare this address exactly, so always copy the one your board shows. FiestaBoard 9.5 through 9.7 show it with `.html` on the end. +3. **Copy the app's client ID into the section.** If the service also gives you a **client secret** and there is a field for it, enter that too. The secret stays on your board. + +Then press **Sign in with** and the service's name. It saves what you entered and starts the sign-in. + +## Disconnecting + +Open the plugin's settings and press **Disconnect**. FiestaBoard deletes the access it was holding. To also remove FiestaBoard from the service's list of connected apps, do that in your account settings on the service. + +Uninstalling a plugin disconnects it. + +## Where your sign-in is kept + +- Access tokens are stored on your board in `data/oauth_tokens.json`, readable only by the FiestaBoard process. +- They are never sent to your browser and are not included in backups, so after restoring a backup onto a new board you connect again. +- Each plugin has its own connection. Two plugins that use the same service do not share access, and neither can see the other's. + +## Troubleshooting + +**"This sign-in can't be passed on."** You opened your board through a public address. Open it by its local address, such as `http://192.168.1.50:4420` or `http://fiestaboard.local:4420`, and connect from there. + +**"That sign-in did not start from this board, or was already used."** The return link was opened twice, or belongs to an earlier attempt. Sign in again. + +**"The sign-in took too long."** You have ten minutes from pressing the sign-in button. Press it again. + +**"The provider rejected the sign-in."** The client ID or secret does not match the app you created, or the app's redirect URI is not exactly the one your board shows. Fix it and sign in again. + +**The service says the redirect URI is invalid or does not match.** The app must list exactly the address shown in the plugin's **Account connection** section. If you set the app up on an older FiestaBoard, add the new address to the app as well. + +**The browser shows "can't reach this page" after signing in.** The device you signed in on cannot reach your board at the address shown. Make sure it is on the same network as the board, or copy the page's whole address and paste it into the plugin's settings (see [When the sign-in doesn't come back](#when-the-sign-in-doesnt-come-back)). + +**The plugin says "Reconnect needed", or "*Service* stopped accepting the sign-in. Sign in again."** The service stopped accepting the stored access, usually because you removed the app from your account or changed your password. Press **Reconnect**. + +**"That doesn't look like a sign-in address or code."** Copy the whole address from the address bar, from `https://` to the end, and paste it again. + +**"That address belongs to a different or finished sign-in."** The pasted address is from an earlier attempt, or was already used. Press **Sign in with** again and paste the new address. + +**"There is no sign-in waiting for a code."** The ten minutes ran out, or the board restarted. Start the sign-in again. + +**The plugin asks for an address, such as Home Assistant's.** Enter it and save before you sign in. It must start with `https://`, or with `http://` only for something on your home network, such as `http://192.168.1.20:8123` or `http://homeassistant.local:8123`. + +**The sign-in button is greyed out.** The plugin needs a client ID first. Paste it into the **Account connection** section. + +**To use a different app**, press **Disconnect**. The setup steps and the client ID field come back. + +**Building a plugin that signs in?** See [Signing In with OAuth](/docs/development/plugin-oauth) in the developer docs. + +**To stop a browser passing sign-ins to a board without asking**, open [the list of remembered boards](https://fiestaboard.app/auth/oauth/boards.html) in that browser and press **Forget**. diff --git a/versioned_docs/version-9.14/features/fiestapanel.md b/versioned_docs/version-9.14/features/fiestapanel.md new file mode 100644 index 0000000000..d8a253c2da --- /dev/null +++ b/versioned_docs/version-9.14/features/fiestapanel.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 7 +description: "Turn any TV or screen with a web browser into a life-size virtual split-flap display — a FiestaPanel is a virtual board you drive with the same pages and schedules as a physical board." +keywords: [FiestaPanel, virtual board, virtual split-flap display, split-flap TV, split flap display, wall display, digital signage, life-size board] +--- + +# FiestaPanel + +FiestaPanel turns any screen with a web browser — most usefully a TV on a wall — into a realistic split-flap display that fills the screen. No Vestaboard hardware required. + +A panel is a **virtual board**: FiestaBoard drives it exactly like a physical board (pages, schedules, plugins, transitions), and a full-screen web page renders whatever was last sent to it, optionally flipping its tiles with the same mechanical animation as the real thing. + +## Overview + +- **Auto-fit, true to life.** Tell FiestaBoard your screen's diagonal size and it builds the largest board that fits: every flap renders at a true-to-life split-flap size, and the grid grows with the screen one character at a time — a 55″ TV gets a 29×12 board (29 columns, 12 rows), a 65″ TV gets 34×14, an 85″ TV gets 45×18. The board is borderless and frameless, filling the screen edge to edge (with up to a 10% gentle stretch to close any remaining gap). +- **Live.** The panel polls for new frames every 2 seconds, so anything that drives the board — a schedule flipping pages, a plugin update, the page editor's Live Output — appears on the TV within moments (with a full mechanical flip animation when you turn it on). +- **No login on the TV.** The panel URL works in any browser with no account or session. You configure everything in the FiestaBoard app; the TV just displays. + +## Quick Setup + +1. Open **Settings → Hardware → FiestaPanel** and click **Create panel**. +2. Give it a name and choose your TV's diagonal size (presets from 32″ to 85″, or a custom value). The board's grid is computed automatically. +3. Open the panel URL in the TV's web browser. Every panel gets a short URL that's easy to type on a TV — the first panel is `/p/1` (for example `http://192.168.1.50:4420/p/1`) — plus a QR code and a copy button in Settings. +4. Give the panel content the same way you would any board: it appears in the board selector, so set its active page, add it to schedules, or point plugins at it. A panel's grid is any number of columns and rows — it is not built from 15×3 Note blocks — and the page editor sizes pages to it for you (see below). + +That's it. The TV shows a blank board until the first frame arrives. + +## Making Pages for a Panel + +A panel's grid is auto-fit from your TV size, so it is almost never one of the stock board shapes — which means a page has to be authored for *that panel*, not for a stock Flagship, Note or Note Array. + +You never have to work that out yourself: + +- **In the page editor**, the board-size picker beside the preview lists your panels by name at the top — "Kitchen TV · 29×12" (columns × rows). Pick one and the page is sized to that panel in one step. The generic Flagship / Note / Note Array choices stay below for real hardware. +- **Everywhere pages are listed**, a page whose grid matches a panel shows a quiet "Fits Kitchen TV" line, so you can tell at a glance which page belongs on which screen. Two panels the same size both fit the same page, and the label says so. +- **Ask FiestaBot.** "Make a page for my Kitchen TV panel" reads the panel's real grid before it builds anything. + +A page is matched to a panel purely by its shape — nothing is stored to tie the two together. So a page fits every panel of that size, and re-fitting a panel (by changing its TV size) leaves pages authored for the old grid behind: the app warns you which ones when it happens. + +Panels created on older versions were fit in whole 15×3 Note blocks, which often left part of the screen dark. After upgrading, FiestaBoard re-fits them per character on startup and moves the pages made for each panel's old grid to its new, larger one, so they keep showing — now across the whole screen. A page is left where it is if a real Note Array still uses that size, or if two panels that used to share it now fit differently. + +## Display Options + +Edit a panel any time — changes reach the TV within about 10 seconds, no reload needed: + +| Setting | What it does | +|---|---| +| **TV size** | Drives everything: the flap scale (pixels-per-inch from your diagonal) and the auto-fit grid (as many real-size flaps — columns and rows — as fit a screen of that size and aspect ratio). Changing it re-fits the board's dimensions. | +| **Flip animation** | Off by default — characters update in place instantly. Turn it on for the full mechanical spin on every change. | +| **Auto-dim** | Fades the panel down during a nightly window (e.g. 22:00–07:00), using the TV's own clock. | +| **Size calibration** | ±15% fine-tune for TVs whose browsers misreport their resolution or overscan the picture. | + +## FiestaPi HDMI Output + +Running FiestaBoard on a FiestaPi? The Pi can drive a TV or monitor directly over HDMI — no browser setup on the TV at all: + +1. Connect a screen to the Pi's HDMI port. +2. In **Settings → Hardware → FiestaPanel**, turn on **HDMI output on this FiestaPi**. The Pi installs a minimal kiosk browser (a few minutes, one time) and boots it onto the screen, pointed at the reserved `/p/display` URL. +3. Turn on **Display output** for the panel you want on that screen. Only one panel holds the role at a time — switching it re-points the screen from the app, no touching the Pi. + +Until a panel is designated, the screen shows a short instruction card. Turn the HDMI switch off to stop the kiosk and reclaim its memory. + +Notes: + +- The switch appears only on FiestaPi installs with the updater sidecar running (it performs the install). If it reports the sidecar is too old, reboot the Pi once — it refreshes the sidecar on every boot — and try again. +- Fresh FiestaPi images also support a flash-time opt-in: drop an empty `fiestapi-hdmi.txt` on the SD card's boot partition and the kiosk is active from first boot. +- Prefer the command line? The same setup is one SSH command — see the [Raspberry Pi guide](https://github.com/Fiestaboard/FiestaBoard/blob/main/docs/internal/setup/RASPBERRY_PI.md). + +## TV Tips + +- **Fullscreen:** click or tap anywhere on the panel to toggle browser fullscreen. Many TV browsers are effectively fullscreen already. +- **Keep the screen awake:** the panel asks the browser for a screen wake lock where supported, but TV sleep timers usually win — disable the TV's screensaver/auto-off for the best result. +- **Mind burn-in on OLED:** a static message is a static image. The panel's background is pure black to minimize lit pixels, and auto-dim helps overnight. +- The cursor hides itself after a few seconds. + +## Troubleshooting + +| Symptom | Meaning | +|---|---| +| Blank board | Nothing has been sent to the panel's virtual board yet — set an active page or wait for its schedule. A restart of FiestaBoard also blanks panels until the next send. | +| Small amber dot in the corner | The TV lost its connection to FiestaBoard; the last frame stays up and the panel keeps retrying. | +| "This panel no longer exists" | The panel was deleted in the app. Create a new one and open its new URL. | +| Board looks the wrong physical size | Double-check the TV size setting, then use **Size calibration** to nudge it true. | +| Grid seems smaller than expected | Auto-fit adds a column or row only when a whole real-size flap fits, so there can be a sliver of margin the ≤10% stretch can't close. Screens smaller than about 28″ (16:9) get a 15-column grid (the width of a Note) that is shrunk slightly to fit. | +| Board drawn too small, or only part of it, after an upgrade | The TV is still running the previous version of the panel page. Reload it once (or power-cycle a kiosk) to pick up the per-character grid. | +| Content looks clipped or misplaced | The panel has its own grid (e.g. 29×12). Pick the panel in the page editor's size picker — pages made for a 22×6 Flagship don't stretch to fill it. | diff --git a/versioned_docs/version-9.14/features/home-assistant-control.md b/versioned_docs/version-9.14/features/home-assistant-control.md new file mode 100644 index 0000000000..b5db607dba --- /dev/null +++ b/versioned_docs/version-9.14/features/home-assistant-control.md @@ -0,0 +1,556 @@ +--- +sidebar_position: 4 +description: "Make FiestaBoard automatically discoverable by Home Assistant via MQTT Discovery. Control your board from HA automations." +keywords: [FiestaBoard Home Assistant, MQTT Discovery, home automation, smart home control, Vestaboard automation] +--- + +# Home Assistant Control (MQTT) + +Make FiestaBoard appear as a native device in Home Assistant — control your board from HA automations and dashboards. + +## Overview + +This feature lets Home Assistant **automatically discover** your FiestaBoard and expose it as a controllable device. Once connected, you can: + +- Turn the schedule on/off from HA +- Switch active pages from HA automations +- Navigate pages sequentially with Next/Previous page buttons +- Send messages to the board when events happen (doorbell, weather alerts, etc.) +- Blank the board or refresh the display remotely +- Change the transition animation style +- See board status, current page, version, and page count on your HA dashboard +- Start/stop the display service remotely +- Monitor device diagnostics (uptime, API mode, active plugins, last display update, output target) +- Trigger automations when the board content changes, pages switch, or settings change (via event entities) +- Track when the board was last updated via a `timestamp` sensor for HA history/statistics + +**No installation needed on the Home Assistant side.** FiestaBoard uses [MQTT Discovery](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery) — the same protocol used by Zigbee2MQTT, Tasmota, and ESPHome — so your board appears automatically in HA. + +## Prerequisites + +Before setting up, you need: + +1. **A running FiestaBoard instance** (v2.2+ when this feature ships) +2. **Home Assistant** with the [MQTT integration](https://www.home-assistant.io/integrations/mqtt/) enabled +3. **An MQTT broker** accessible from both FiestaBoard and Home Assistant + +:::tip First time connecting? +See the [Connecting to Home Assistant via MQTT](/docs/setup/home-assistant-mqtt) guide for a step-by-step walkthrough of broker setup, HA MQTT integration configuration, and FiestaBoard configuration. +::: + +### About the MQTT Broker + +An MQTT broker relays messages between FiestaBoard and Home Assistant. The most common setup: + +- **Mosquitto add-on** — If you run Home Assistant OS or Supervised, install the [Mosquitto broker add-on](https://github.com/home-assistant/addons/tree/master/mosquitto) from the HA Add-on Store (Settings → Add-ons → Mosquitto broker). This is a one-click install and the most popular HA add-on. +- **Existing broker** — If you already have an MQTT broker on your network (common if you use Zigbee2MQTT, Tasmota, or other IoT devices), you can reuse it. +- **Standalone Mosquitto** — You can run Mosquitto on any machine. See the [Eclipse Mosquitto documentation](https://mosquitto.org/documentation/) for installation instructions. + +:::info +Most Home Assistant users already have an MQTT broker. If you use Zigbee2MQTT, Tasmota, ESPHome, or Frigate, you already have one. +::: + +## Setup + +### Step 1: Enable MQTT in FiestaBoard + +1. Open **FiestaBoard** at `http://:4420` +2. Go to **Settings** +3. Find the **Home Assistant (MQTT)** section +4. Toggle **Enable MQTT** on +5. Enter your MQTT broker details: + +| Setting | Description | Example | +|---------|-------------|---------| +| **Broker Host** | Hostname or IP of your MQTT broker | `192.168.1.100` or `homeassistant.local` | +| **Broker Port** | MQTT broker port (standard: 1883) | `1883` | +| **Username** | MQTT username (if broker requires auth) | `mqtt_user` | +| **Password** | MQTT password (if broker requires auth) | `mqtt_pass` | + +1. Click **Save** — FiestaBoard will connect to the broker and publish discovery messages + +### Step 2: Verify in Home Assistant + +1. Open **Home Assistant** +2. Go to **Settings → Devices & Services → MQTT** +3. Look for a device named **FiestaBoard** +4. Click it to see all available controls and sensors + +That's it. FiestaBoard is now controllable from Home Assistant. + +:::tip +If FiestaBoard doesn't appear, check: + +- The MQTT broker is running and reachable from both FiestaBoard and HA +- The MQTT integration is enabled in HA (Settings → Devices & Services → Add Integration → MQTT) +- FiestaBoard shows "MQTT Connected" in its Settings page + +::: + +## What You Get in Home Assistant + +Once connected, FiestaBoard appears as a device with **25 entities** — controls, sensors, diagnostics, and events: + +### Controls (you can change these from HA) + +| Entity | Type | Description | +|--------|------|-------------| +| **Schedule** | Switch | Turn the FiestaBoard schedule on/off | +| **Display Service** | Switch | Start/stop the FiestaBoard display service | +| **Board Paused** | Switch | Pause/resume the primary board. While on, all writes stop, including manual messages, page selections, collection rotation and scheduled updates. | +| **Active Page** | Select | Choose a page or collection to display. Collections appear as `Collection: Name`; labels that collide include the collection's id. The options refresh automatically when pages or collections are added, renamed or removed. Selecting a target always re-sends it, even if already active, so it restores content after a manual message. The `None` option represents no active page; as a command it only applies to secondary boards, since the primary board never goes dark. | +| **Transition Style** | Select | Board transition animation (column, reverse-column, edges-to-center, row, diagonal, random) | +| **Send Message** | Text | Send a text message to the board (up to 255 characters — Home Assistant's text-entity ceiling; markup like `{red}` counts as characters here but as one flap on the board) | +| **Refresh Display** | Button | Force a display refresh. This is a *force* refresh: unchanged content is re-sent, so it restores the active page after something else wrote to the board. | +| **Blank Board** | Button | Clear the board display (all blank) | +| **Next Page** | Button | Navigate to the next page in the page list (wraps around) | +| **Previous Page** | Button | Navigate to the previous page in the page list (wraps around) | +| **Refresh Interval** | Number | Set how often the board refreshes (30–3600 seconds). *Categorized as config.* | + +**The Schedule switch repaints the board.** It selects where the board's +content comes from — the schedule while on, the manually selected page while +off — so both edges of the switch change the board on the next polling pass. +To make re-enabling wait until the schedule reaches its next window, turn on +*Wait for the next scheduled change* in Settings → Behavior (see +[Schedule Mode](/docs/features/schedule)). To leave the board untouched +entirely while an automation runs, turn on the **Board Paused** switch instead +of the Schedule switch. + +**Board Paused is a hands-off control.** Turn it on before quiet hours or +unplugging a board. Page selections can still be saved while paused, but +FiestaBoard sends nothing until you turn it off. It uses the same pause state +as Settings → Boards, and the display loop resumes the selected content on +its next pass. Stopping Display Service only stops the background loop; +Board Paused also blocks manual writes. + +The discovered switch controls the primary board. To pause a secondary board, +publish `{"paused": true, "board_id": "YOUR_BOARD_ID"}` to +`fiestaboard/board_paused/set`; use `false` to resume it. The switch state +continues to report the primary board. To select a collection on a secondary +board, publish `{"page": "Collection: Morning", "board_id": "YOUR_BOARD_ID"}` +to `fiestaboard/active_page/set`. Replace `fiestaboard` if you use a custom +MQTT base topic. + +**Manual writes stay on the board.** Send Message, Blank Board and the debug +endpoints write straight to the board and are left there — the display loop +will not paint over them on its next cycle. While manual content is displayed, +the Active Page select and Current Page sensor report `None` rather than the +configured page, so Home Assistant shows that the board is not on a page. The +configured page itself is kept as the restore target: put it back with +**Refresh Display**, by selecting a page (re-selecting the same page works), +or by letting that page's content change on its own — the entities switch back +to the page name as soon as the page lands on the board. + +**Send Message formatting:** text longer than the board width word-wraps onto +the following rows automatically, sized to your board's real geometry (6 rows × +22 columns on Flagship, 3 × 15 on Note). Width is counted in flaps, so a colour +marker like `{red}` costs one tile, not five characters. A word wider than the +board is split across rows rather than cut off. Lines beyond the board's row +count are dropped. + +The text entity is single-line, so to force a line break type the literal +two-character sequence `\n` where you want the break (e.g. `TACO\nTUESDAY`) — +wrapping still fills within those breaks. If your text needs a real backslash +before an `n`, double it: `C:\\new` renders as `C:\new`. Any other backslash +(`C:\temp`) is left alone. This escape applies only to the plain-string +payload the text entity publishes; a JSON payload such as +`{"message": "TACO\nTUESDAY", "board": "Kitchen"}` can carry a real newline and +is never rewritten. + +### Sensors (read-only status from FiestaBoard) + +| Entity | Type | Description | +|--------|------|-------------| +| **Current Page** | Sensor | Name of the page currently displayed. *Includes JSON attributes (page_id, page_index).* | +| **Board Message** | Sensor | Summary of what's currently on the board | +| **Silence Mode** | Binary Sensor | Whether quiet hours are active on the **primary** board (silence is per board — see [Silence Schedule](./silence-schedule.md)) | +| **Page Count** | Sensor | Number of pages configured *(state_class: measurement)* | + +### Diagnostics (device health and info, grouped separately in HA) + +These entities are tagged with `entity_category: diagnostic` so Home Assistant groups them in the **Diagnostic** section of the device page, keeping your main dashboard clean. + +| Entity | Type | Description | +|--------|------|-------------| +| **Service Status** | Binary Sensor | Whether FiestaBoard is running *(device_class: running)* | +| **Version** | Sensor | FiestaBoard software version | +| **Uptime** | Sensor | How long FiestaBoard has been running *(device_class: duration, unit: seconds)* | +| **Board API Mode** | Sensor | Whether the board is using Local API or Cloud API | +| **Active Plugins** | Sensor | Number of enabled plugins *(state_class: measurement)* | +| **Last Display Update** | Sensor | ISO timestamp of when the board content was last changed *(device_class: timestamp)* | +| **Output Target** | Sensor | Current output mode: `ui`, `board`, or `both` | + +### Events (trigger automations when things happen) + +FiestaBoard publishes [MQTT event entities](https://www.home-assistant.io/integrations/event.mqtt/) that let you build automations based on board activity — no polling required. + +| Entity | Event Types | Description | +|--------|------------|-------------| +| **Display Updated** | `message_sent`, `page_refreshed`, `board_blanked`, `page_navigated` | Fires when the board content changes | +| **Page Changed** | `page_switched` | Fires when the active page changes | +| **Settings Changed** | `schedule_toggled`, `service_toggled`, `silence_mode_changed` | Fires when schedule, display service, or silence mode state changes | + +All events include a `timestamp` field with the UTC ISO 8601 time of the event. + +## Automation Examples + +### Send a Welcome Home Message + +When someone arrives home, display a welcome message on the board: + +```yaml +automation: + - alias: "Welcome Home Board Message" + trigger: + - platform: state + entity_id: person.john + to: "home" + action: + - service: text.set_value + target: + entity_id: text.fiestaboard_send_message + data: + value: "Welcome Home John!" +``` + +### Show Weather Page When Raining + +Automatically switch to the weather page when rain is detected: + +```yaml +automation: + - alias: "Show Weather When Raining" + trigger: + - platform: state + entity_id: weather.home + attribute: condition + to: "rainy" + action: + - service: select.select_option + target: + entity_id: select.fiestaboard_active_page + data: + option: "Weather Dashboard" +``` + +### Blank the Board at Bedtime + +Clear the board at night and restart in the morning: + +```yaml +automation: + - alias: "Blank Board at Bedtime" + trigger: + - platform: time + at: "22:00:00" + action: + - service: button.press + target: + entity_id: button.fiestaboard_blank_board + - alias: "Turn On Board in Morning" + trigger: + - platform: time + at: "07:00:00" + action: + - service: switch.turn_on + target: + entity_id: switch.fiestaboard_display_service +``` + +### Log Board Updates (using events) + +Use the **Display Updated** event entity to log or react when the board content changes: + +```yaml +automation: + - alias: "Log Board Message Sent" + trigger: + - platform: state + entity_id: event.fiestaboard_display_updated + attribute: event_type + to: "message_sent" + action: + - service: logbook.log + data: + name: "FiestaBoard" + message: "A new message was sent to the board" +``` + +### Notify When Page Changes (using events) + +Get a notification when someone switches the active page: + +```yaml +automation: + - alias: "Notify on Page Change" + trigger: + - platform: state + entity_id: event.fiestaboard_page_changed + action: + - service: notify.mobile_app + data: + title: "FiestaBoard" + message: "Page changed to {{ trigger.to_state.attributes.page_name }}" +``` + +### Cycle Pages Every Hour + +Use the Next Page button to rotate through your pages on a schedule: + +```yaml +automation: + - alias: "Cycle Pages Hourly" + trigger: + - platform: time_pattern + minutes: 0 + action: + - service: button.press + target: + entity_id: button.fiestaboard_next_page +``` + +### React to Silence Mode Changes (using events) + +Turn off smart lights when silence mode activates (e.g., at bedtime): + +```yaml +automation: + - alias: "Silence Mode Bedtime" + trigger: + - platform: state + entity_id: event.fiestaboard_settings_changed + attribute: event_type + to: "silence_mode_changed" + condition: + - condition: state + entity_id: binary_sensor.fiestaboard_silence_mode + state: "on" + action: + - service: light.turn_off + target: + entity_id: light.living_room +``` + +### Display on HA Dashboard + +Add FiestaBoard status and controls to your HA dashboard: + +```yaml +type: entities +title: FiestaBoard +entities: + - entity: binary_sensor.fiestaboard_service_status + - entity: sensor.fiestaboard_current_page + - entity: sensor.fiestaboard_board_message + - entity: sensor.fiestaboard_page_count + - entity: switch.fiestaboard_schedule + - entity: switch.fiestaboard_display_service + - entity: select.fiestaboard_active_page + - entity: button.fiestaboard_next_page + - entity: button.fiestaboard_previous_page + - entity: select.fiestaboard_transition_style + - entity: number.fiestaboard_refresh_interval +``` + +:::tip Diagnostics on their own card +Diagnostic entities (version, uptime, API mode, active plugins) are automatically grouped under the **Diagnostic** section on the device page in HA. You can also add them to a separate dashboard card: + +```yaml +type: entities +title: FiestaBoard Diagnostics +entities: + - entity: sensor.fiestaboard_version + - entity: sensor.fiestaboard_uptime + - entity: sensor.fiestaboard_board_api_mode + - entity: sensor.fiestaboard_active_plugins + - entity: sensor.fiestaboard_last_display_update + - entity: sensor.fiestaboard_output_target +``` + +::: + +## Configuration Reference + +### Environment Variables + +You can also configure MQTT via environment variables (useful for Docker): + +| Variable | Default | Description | +|----------|---------|-------------| +| `MQTT_ENABLED` | `false` | Enable MQTT integration | +| `MQTT_BROKER_HOST` | `localhost` | MQTT broker hostname or IP | +| `MQTT_BROKER_PORT` | `1883` | MQTT broker port | +| `MQTT_USERNAME` | *(empty)* | MQTT username | +| `MQTT_PASSWORD` | *(empty)* | MQTT password | +| `MQTT_DISCOVERY_PREFIX` | `homeassistant` | HA discovery topic prefix | +| `MQTT_BASE_TOPIC` | `fiestaboard` | Base topic for state and commands | +| `MQTT_INSTANCE_ID` | `fiestaboard_1` | Unique ID (for multi-board setups) | +| `FIESTABOARD_EXTERNAL_URL` | *(empty)* | URL shown as the "Visit" link on the HA device page (e.g. `http://192.168.1.50:4420`). When not set, no link appears. | + +### Docker Compose Example + +```yaml +services: + fiestaboard: + image: fiestaboard/fiestaboard:latest + ports: + - "4420:4420" + environment: + - MQTT_ENABLED=true + - MQTT_BROKER_HOST=192.168.1.100 + - MQTT_BROKER_PORT=1883 + - MQTT_USERNAME=mqtt_user + - MQTT_PASSWORD=mqtt_pass + - FIESTABOARD_EXTERNAL_URL=http://192.168.1.50:4420 +``` + +### Multiple FiestaBoards + +If you have more than one FiestaBoard, give each a unique instance ID: + +```env +# Living room board +MQTT_INSTANCE_ID=fiestaboard_living_room +MQTT_BASE_TOPIC=fiestaboard_living_room + +# Office board +MQTT_INSTANCE_ID=fiestaboard_office +MQTT_BASE_TOPIC=fiestaboard_office +``` + +Each board appears as a separate device in Home Assistant. + +## How It Works + +FiestaBoard uses [MQTT Discovery](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery) — the same mechanism used by Zigbee2MQTT, Tasmota, ESPHome, and Frigate. Here's the flow: + +1. FiestaBoard connects to the MQTT broker +2. It publishes **discovery messages** to topics under `homeassistant/` — these tell HA what entities to create, including entity categories (diagnostic, config) and sensor classes +3. It publishes an **availability message** (`online`) so HA knows the board is connected +4. It publishes **current state** for all entities, plus **JSON attributes** for entities that carry extra metadata (e.g., the current page's ID and index) +5. Home Assistant reads the discovery messages and **automatically creates the device** with all entities, properly categorized in the UI +6. When you interact with an entity in HA (toggle a switch, send a message), HA publishes a **command** to the MQTT broker +7. FiestaBoard receives the command, executes it, publishes the **updated state** back, and fires relevant **events** (e.g., `display_updated`, `page_changed`) + +All state messages are **retained** — if HA restarts, it immediately gets the current state without waiting for a change. + +FiestaBoard sets a **Last Will and Testament (LWT)** — if FiestaBoard disconnects unexpectedly, the broker automatically publishes `offline` so HA knows the board is down. + +### Entity Categories + +FiestaBoard properly categorizes entities using Home Assistant's `entity_category` system: + +- **No category** — Main controls and sensors (Schedule, Active Page, Send Message, etc.) — shown prominently in dashboards +- **Diagnostic** — Device health info (Version, Uptime, API Mode, Active Plugins, Service Status) — grouped in the Diagnostic section on the device page +- **Config** — Settings (Refresh Interval) — grouped in the Configuration section + +## Troubleshooting + +### FiestaBoard Not Appearing in HA + +1. **Check MQTT broker connectivity** — Verify FiestaBoard can reach the broker (check FiestaBoard Settings page for connection status) +2. **Check HA MQTT integration** — Go to HA → Settings → Devices & Services and ensure the MQTT integration is configured and connected to the same broker +3. **Check broker logs** — Look at the Mosquitto broker logs for connection/authentication errors +4. **Restart discovery** — Disable and re-enable MQTT in FiestaBoard settings to republish discovery messages + +### Entities Showing "Unavailable" + +- FiestaBoard may have disconnected — check the FiestaBoard service is running +- Check the MQTT broker is running +- The LWT message marks the board as offline if the connection drops + +### Commands Not Working + +- Verify the MQTT broker allows publish/subscribe from both HA and FiestaBoard +- Check that HA's MQTT integration is configured with the correct broker credentials +- Check FiestaBoard logs for command processing errors + +### A Command Stopped Working After Renaming a Board + +Command payloads can target a specific board with a `board` field, which +accepts either the board's id or its display name: + +```json +{ "board": "Kitchen", "page_id": "weather" } +``` + +Display names are editable in **Settings → Boards**, so renaming a board +breaks every automation that targets it by the old name — FiestaBoard cannot +rewrite payloads that live in your Home Assistant config. When this happens +the log records: + +```text +MQTT: resolved board by name 'Kitchen'; renaming this board will break it. Target the board id 'b1' instead. +``` + +Target the board **id** instead. Ids never change, and they are what the +discovery topics already use, so renaming a board never orphans its HA +entities — only name-based `board` refs in your own automations. + +## Branding + +### Entity Icons + +Each entity in HA shows a Material Design Icon that reflects its role: + +| Entity | Icon | Reasoning | +|--------|------|-----------| +| Schedule | `mdi:calendar-clock` | Scheduled content delivery | +| Display Service | `mdi:television-play` | The board as an active display | +| Board Paused | `mdi:pause-circle` | Prevent every write to the board | +| Active Page | `mdi:bookmark` | The page queued for display | +| Transition Style | `mdi:animation-play` | Animated flip between pages | +| Current Page | `mdi:eye` | What is visible on the board right now | +| Board Message | `mdi:text` | The text content on the board | +| Silence Mode | `mdi:volume-off` | Quiet hours: no updates | +| Blank Board | `mdi:rectangle-outline` | Clear/empty board face | +| Send Message | `mdi:send` | Push text to the board | +| Next Page | `mdi:page-next` | Navigate to the next page | +| Previous Page | `mdi:page-previous` | Navigate to the previous page | +| Uptime | `mdi:clock-outline` | How long the service has been running | +| Board API Mode | `mdi:api` | Local or Cloud API connection | +| Active Plugins | `mdi:puzzle` | Number of enabled data sources | +| Last Display Update | `mdi:clock-check-outline` | When the board was last updated | +| Output Target | `mdi:monitor-speaker` | Where output is sent (ui/board/both) | +| Display Updated | `mdi:update` | Board content changed event | +| Page Changed | `mdi:book-open-page-variant` | Active page switched event | +| Settings Changed | `mdi:cog` | Schedule/service/silence state changed | + +### Device Image + +MQTT Discovery does not support setting a device-level image directly. To add a FiestaBoard logo to the device page in HA: + +1. Go to **Settings → Devices & Services → MQTT** +2. Click on the **FiestaBoard** device +3. Click the **pencil (edit)** icon in the top-right corner +4. Under **Icon**, click the camera/image icon and upload the FiestaBoard logo + - The logo is served from your FiestaBoard instance at `http://:4420/logo.png` + +This is saved locally in HA and persists across restarts. + +## FAQ + +### Do I need to install anything in Home Assistant? + +No. FiestaBoard uses MQTT Discovery, which is built into Home Assistant's MQTT integration. No custom components, no HACS, no YAML configuration needed. + +### What if I don't have an MQTT broker? + +The easiest option is to install the [Mosquitto broker add-on](https://github.com/home-assistant/addons/tree/master/mosquitto) in Home Assistant (one click from the Add-on Store). Alternatively, you can run [Mosquitto](https://mosquitto.org/) on any machine on your network. + +### Is MQTT secure? + +MQTT supports username/password authentication. For additional security, you can configure your broker to use TLS encryption (port 8883). FiestaBoard sends credentials securely to the broker. + +### Does this affect FiestaBoard if I don't use Home Assistant? + +No. MQTT integration is **disabled by default** and completely opt-in. If you don't enable it, there's zero impact on FiestaBoard's behavior or performance. + +### What is MQTT? + +[MQTT](https://mqtt.org/) is an open standard messaging protocol (ISO/IEC 20922) designed for IoT devices. It's lightweight, reliable, and the most widely used protocol for smart home device communication. It's used by Zigbee2MQTT, Tasmota, ESPHome, Frigate, and hundreds of other smart home projects. + +## Related + +- [Home Assistant Plugin](/plugins/detail?id=home_assistant) — Display HA entity states *on* your board (the reverse direction) +- [Plugin Directory](/plugins) — See all available plugins diff --git a/versioned_docs/version-9.14/features/localization.md b/versioned_docs/version-9.14/features/localization.md new file mode 100644 index 0000000000..9f311b118b --- /dev/null +++ b/versioned_docs/version-9.14/features/localization.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 5 +description: "Display your FiestaBoard in multiple languages with community-maintained translations for the web UI and board content." +keywords: [FiestaBoard localization, i18n, translations, languages, internationalization, split-flap languages, Vestaboard languages] +--- + +# Localization (i18n) + +FiestaBoard supports multiple languages through community-maintained translations. + +## Supported Languages + +| Language | Code | Status | +|---------------------|------|--------| +| English | `en` | ✅ Default | +| Spanish | `es` | ✅ Available | +| French | `fr` | ✅ Available | +| German | `de` | ✅ Available | +| Italian | `it` | ✅ Available | +| Portuguese | `pt` | ✅ Available | +| Dutch | `nl` | ✅ Available | +| Polish | `pl` | ✅ Available | +| Russian | `ru` | ✅ Available | +| Swedish | `sv` | ✅ Available | +| Turkish | `tr` | ✅ Available | +| Japanese | `ja` | ✅ Available | +| Korean | `ko` | ✅ Available | +| Chinese (Simplified)| `zh` | ✅ Available | + +## Changing Language + +Use the language selector in the sidebar footer to switch between available languages. Your preference is saved and persists across sessions. + +## About Translations + +:::info Community Maintained +Translations in FiestaBoard were initially generated with the help of an LLM (Large Language Model) and are community maintained. While LLM-generated translations provide a solid starting point, they may not always capture the perfect phrasing or local idioms for every language. + +We welcome and encourage contributions from native speakers to improve translation quality. +::: + +## Contributing Translations + +Translation files are located in `web/messages/` as JSON files (one per language). To improve an existing translation or add a new language: + +1. **Improve existing translations**: Edit the appropriate file in `web/messages/` (e.g., `es.json` for Spanish) +2. **Add a new language**: + - Copy `web/messages/en.json` to `web/messages/{locale}.json` + - Translate all values (keep the JSON keys unchanged) + - Add the locale to `web/src/i18n/config.ts` +3. **Submit a pull request** with your changes + +### Translation File Structure + +Each translation file is a JSON object organized by feature namespace: + +```json +{ + "common": { + "save": "Save", + "cancel": "Cancel" + }, + "navigation": { + "home": "Home", + "pages": "Pages" + } +} +``` + +### Guidelines + +- Keep `{variable}` interpolation placeholders exactly as they are +- Use ICU message format for plurals: `{count, plural, one {# page} other {# pages}}` +- Don't translate the brand names "FiestaBoard" or "Vestaboard" +- Technical terms (API, URL, JSON) should generally stay in English +- Test your translations by switching to the language in the UI + +### Lint Rule + +An ESLint rule (`i18next/no-literal-string`) is configured to warn when hardcoded strings are found in JSX that should be using translations. Run `npm run lint` in the `web/` directory to check for missed translations. diff --git a/versioned_docs/version-9.14/features/page-editor.md b/versioned_docs/version-9.14/features/page-editor.md new file mode 100644 index 0000000000..9d8a8f3d2f --- /dev/null +++ b/versioned_docs/version-9.14/features/page-editor.md @@ -0,0 +1,235 @@ +--- +sidebar_position: 1 +description: "Use FiestaBoard's WYSIWYG page editor to create and design custom content for your split-flap display, with optional AI-assisted page drafting." +keywords: [FiestaBoard page editor, WYSIWYG, display content, custom pages, split-flap editor, Vestaboard editor, Vestaboard AI, AI page generation, LLM page drafts] +--- + +# Page Editor + +FiestaBoard's WYSIWYG page editor lets you create and edit board display pages with a visual editor that shows exactly how content will appear on your split-flap display. + +## Overview + +The page editor is the core of FiestaBoard's content creation experience. It provides a real-time preview of your board layout, template variable insertion, and color formatting, all in a visual interface. + + + +## Device Types + +FiestaBoard supports multiple Vestaboard devices with different display sizes: + +| Device | Dimensions | Total Characters | +|--------|-----------|------------------| +| **Flagship** | 6 rows × 22 columns | 132 | +| **Note** | 3 rows × 15 columns | 45 | + +Pages are device-specific. The **Pages** section has **Flagship** and **Note** tabs to organize pages by device type. The editor and live preview automatically adapt to the selected device's dimensions. + +:::tip +The Vestaboard Note supports a heart character (❤) at code 62, which displays as a degree symbol (°) on the Flagship. +::: + +## Creating a New Page + +1. Open the FiestaBoard Web UI at `http://localhost:4420` +2. Navigate to the **Pages** section +3. Select the **Flagship** or **Note** tab for your target device +4. Click **New** +5. Give your page a name +6. Use the editor to compose your content (dimensions match your target device) +7. Click **Save** + +## Using Template Variables + +Template variables let you insert live data from your enabled plugins into any page. Variables are automatically replaced with real-time data when the page is displayed. + +### Inserting Variables + +1. In the page editor, click the **Variables** button +2. Browse available variables grouped by plugin +3. Click a variable to insert it at the cursor position + + + +### Variable Types + +| Type | Description | Example | +|------|-------------|---------| +| **Simple** | Single value replacement | `{{weather.temperature}}` → `72°F` | +| **Array** | Multi-line data | `{{stocks.prices}}` → formatted stock rows | + +### Example Variables + +Here are some commonly used template variables: + +- `{{weather.temperature}}` - Current temperature +- `{{weather.condition}}` - Weather conditions (Sunny, Cloudy, etc.) +- `{{stocks.prices}}` - Stock price display +- `{{date_time.datetime}}` - Current date and time +- `{{muni.formatted}}` - Transit arrival times + +### Inline Formulas (`{{= ... }}`) + +For anything beyond a plain value — `IF` / `ELSE`, comparisons, math, string +manipulation, conditional colors — FiestaBoard supports an Excel-style +formula language inside `{{= ... }}` blocks: + +```text +{{= IF(weather.temperature > 80, "HOT", "OK") }} +{{= IF(stocks.AAPL.change >= 0, COLOR("green"), COLOR("red")) }} AAPL +``` + +Formulas also handle the two things plain variables can't: + +```text +{{= COUNT(mlb.games) }} GAMES +{{= FOREACH(mlb.games, item.team1 & " " & item.score1, 4) }} +{{= DATEDIFF(TODAY(), DATE(launch.day)) }} DAYS TO GO +``` + +When a plugin gives you a **list** — today's games, the next departures — use +`COUNT` and `FOREACH` rather than writing one line per possible item. +`FOREACH` returns one board row per item and fills the rows below it, so leave +those rows empty and give it a limit that fits. + +See the [Template Formulas reference](/docs/reference/template-formulas) for +the complete syntax, function list, and recipes. + +## AI Assistant (optional) + +FiestaBoard includes **FiestaBot**, an AI chat assistant that can draft +and edit board pages through natural conversation. Click **AI Assistant** +in the sidebar (the Sparkles icon) to open the panel. + + + +FiestaBoard ships **without any bundled LLM credentials**. You bring +your own provider, your own API key, and your own model list. Two +wire-format protocols are supported out of the box: + +- **OpenAI-compatible** chat completions — works with OpenRouter, + OpenAI, DeepSeek, Mistral, Together AI, Fireworks AI, Ollama, + LM Studio, vLLM, llama.cpp, and most other local servers. +- **Anthropic Messages API** — direct Claude access via + `api.anthropic.com`. + +### How it works + +1. Click **AI Assistant** in the sidebar to open the FiestaBot panel. +2. Type a description of what you want — for example, *"Build a weather + and transit page for my morning commute"* or *"Replace line 2 with + today's date"*. +3. FiestaBot shows its work: each action appears in the panel as it + runs, and the app takes you to the page, schedule or setting it is + changing so you can watch the values land. +4. Continue the conversation to refine the result, ask questions, or + request a completely different layout. When a request is ambiguous + FiestaBot asks first. +5. Anything that cannot be undone — deleting a page or schedule, + uninstalling a plugin — waits for your **Approve**. **Stop** ends a + turn at any point. + +When a page is open in the editor, FiestaBot automatically has context +of what you're working on — no need to attach anything manually. The +panel is also available from any other section of the app, where it can +answer questions about plugins, variables, and FiestaBoard features. + +### What FiestaBot can do + +- **Create and edit pages** — draft full template layouts or make + targeted edits (change a line, add a variable, adjust alignment). +- **Suggest variables** — recommend which plugin variables to use for + a given goal and explain what they do. +- **Install plugins** — recommend and install a plugin from the registry + when it's needed for what you're building. +- **Configure settings** — update display, polling, location, or silence + schedule settings on your behalf. +- **Create and edit schedules and collections** — build automation around + the pages you create. +- **Everything the MCP server can do** — FiestaBot uses the same tools an + external MCP client (Claude Desktop, Claude Code) would, so there is one + set of capabilities, not two. + +### Setup + +The **AI Assistant** item only appears in the sidebar after you've +configured at least one provider in **Settings → AI Providers**. See the +[AI Providers setup guide](/docs/setup/ai-providers) for the full +walkthrough, recommended models, and a privacy note covering exactly +what FiestaBoard sends to your provider. + +## Working with Colors + +The split-flap display supports colored tiles using special character codes. You can use these in the editor to add visual emphasis to your pages. + + + +| Code | Color | Common Use | +|------|-------|------------| +| `{{63}}` | 🟥 Red | Alerts, high temperatures | +| `{{64}}` | 🟧 Orange | Warm temperatures | +| `{{65}}` | 🟨 Yellow | Comfortable, warnings | +| `{{66}}` | 🟩 Green | Good status, success | +| `{{67}}` | 🟦 Blue | Cold temperatures, info | +| `{{68}}` | 🟪 Violet | Very cold, accents | +| `{{69}}` | ⬜ White | Backgrounds | +| `{{70}}` | ⬛ Black | Backgrounds | + +See the [Color Guide](/docs/reference/color-guide) for detailed usage examples. + +## Page Transitions + +The editor's header toolbar has a **Transition** dropdown that controls how the board animates when it changes to this page. + +- **Use global default** - inherit the transition set in **Settings → Behavior → Board Transitions** +- A **built-in strategy** (Wave, Drift, Curtain, Row, Diagonal, Random) - performed by the board itself, Local API only +- A **transition plugin** (Typewriter, Simple Dissolve, and others) - animated by FiestaBoard, works on any connection, shown only when the Transition Plugins beta is enabled + +Anything other than **Use global default** becomes a page-level override that applies whenever this page is sent. Switching back to **Use global default** clears it. + +See [Transitions](/docs/features/transitions) for how the two kinds differ, what they require, and how to preview them in the Transition Lab. + +## Page Layout + +The board display dimensions depend on the target device: + +- **Flagship**: 6 rows × 22 columns (132 characters) +- **Note**: 3 rows × 15 columns (45 characters) + +The editor reflects the target device's layout so you can see exactly how your content will be positioned. + +### Tips for Good Layouts + +- **Keep text concise** - You have limited characters per row (22 for Flagship, 15 for Note) +- **Use alignment** - Center important information for readability +- **Mix data sources** - Combine multiple plugin variables on one page +- **Test with preview** - Use the live preview to check formatting before saving +- **Design per device** - Create separate pages optimized for each device's dimensions + +## Managing Pages + + + +### Editing Existing Pages + +1. Go to the **Pages** section +2. Click on any page to open it in the editor +3. Make your changes +4. Click **Save** + +### Deleting Pages + +1. Go to the **Pages** section +2. Click the delete option on the page you want to remove +3. Confirm the deletion + +:::tip +Pages that are referenced in schedules should be updated or removed from the schedule before deleting. +::: + +## Next Steps + +- [Transitions](/docs/features/transitions) - Choose how the board animates into this page +- [Schedule Mode](/docs/features/schedule) - Automate when pages are displayed +- [Plugin Directory](/plugins) - See available data sources for your pages +- [Color Guide](/docs/reference/color-guide) - Learn about color formatting options diff --git a/versioned_docs/version-9.14/features/schedule.md b/versioned_docs/version-9.14/features/schedule.md new file mode 100644 index 0000000000..7e9ad973a6 --- /dev/null +++ b/versioned_docs/version-9.14/features/schedule.md @@ -0,0 +1,116 @@ +--- +sidebar_position: 2 +description: "Configure FiestaBoard's schedule mode to automatically rotate pages on your split-flap display at set times." +keywords: [FiestaBoard schedule, page rotation, timed display, auto rotate, scheduled content, Vestaboard scheduler] +--- + +# Schedule Mode + +FiestaBoard's schedule mode lets you automate which pages display on your board at different times and days. Use the visual calendar interface to set up time-based page rotation. + +## Overview + +The schedule feature provides a visual calendar where you can assign pages to specific time slots throughout the week. This is perfect for: + +- Showing weather in the morning and stocks during market hours +- Displaying transit times during your commute +- Showing fun content like Star Trek quotes on weekends +- Setting a default "home" page for unscheduled times + + + +## Creating a Schedule Entry + +1. Open the FiestaBoard Web UI at `http://localhost:4420` +2. Navigate to the **Schedule** page +3. Click to create a new schedule entry +4. Configure the entry: + - **Page** - Select which page to display + - **Start Time** - When the page should start showing + - **End Time** - When the page should stop showing + - **Days** - Which days this schedule applies to + + + +### Day Patterns + +| Pattern | Days | +|---------|------| +| **All Days** | Monday through Sunday | +| **Weekdays** | Monday through Friday | +| **Weekends** | Saturday and Sunday | +| **Custom** | Select individual days | + +### Time Granularity + +Schedule times are set in **15-minute increments** and displayed in your local timezone. + +## Calendar and List Views + +The schedule page offers two views: + +### Calendar View + +The calendar view provides a visual weekly overview. Each scheduled page appears as a colored block on the timeline, making it easy to see your full schedule at a glance. + +### List View + +The list view shows all schedule entries in a table format, which is useful for managing many entries or making quick edits. + + + +## Schedule Validation + +FiestaBoard automatically validates your schedule and provides real-time feedback: + +- **Overlap Detection** - Warns if two schedules overlap on the same day/time +- **Gap Detection** - Shows unscheduled time periods +- **Active Page Resolution** - The system checks at your configured polling interval (default: 15 seconds) which page should be displayed based on the current time + +:::info +When no schedule is active, FiestaBoard displays the default page. You can set the default page in Settings. +::: + +## How Page Resolution Works + +At your configured polling interval (default: 15 seconds), the display service: + +1. Checks the current local time and day of week +2. Looks for enabled schedule entries that match the current time +3. If a match is found, displays that page +4. If no match is found, displays the default page + +> **Note:** Schedules can cross midnight (e.g., 11 PM to 1 AM). The system handles the rollover automatically. + +## Turning the Schedule Off and On + +Schedule mode is a *source* switch, not a freeze: while it is on the board +follows the schedule, and while it is off it follows the manually selected +page. Because the active page is re-resolved on every polling pass, turning +the schedule back on normally repaints the board straight away — it jumps to +whichever window is current. + +If you drive the switch from an automation (say, turning the schedule off +while a film is playing), you may prefer the board to stay put until it would +have changed on its own. Turn on **Settings → Behavior → Schedule toggle → +"Wait for the next scheduled change"**. Re-enabling then records the schedule +entry winning at that moment and keeps the board on its manual page until a +different entry takes over. Gaps between entries count as a window of their +own, so the toggle never repaints the board by itself. + +The setting is off by default and applies to every board. + +> **Tip:** To leave the board completely untouched — rather than switching it +> to the manual page — pause the board or turn off the Display Service switch, +> instead of disabling the schedule. + +## Known Limitations + +- **Switching delay** - There may be a delay of up to one refresh interval when switching between scheduled pages. +- **Recommended limit** - For optimal performance, keep the total number of schedule entries under 50. + +## Next Steps + +- [Page Editor](/docs/features/page-editor) - Create pages to use in your schedule +- [Silence Schedule](/docs/features/silence-schedule) - Set quiet hours for your board +- [Plugin Directory](/plugins) - Add data sources to your pages diff --git a/versioned_docs/version-9.14/features/silence-schedule.md b/versioned_docs/version-9.14/features/silence-schedule.md new file mode 100644 index 0000000000..c08b5cab28 --- /dev/null +++ b/versioned_docs/version-9.14/features/silence-schedule.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 3 +description: "Set quiet hours on your FiestaBoard to silence split-flap display updates during nighttime or specific periods." +keywords: [FiestaBoard silence schedule, quiet hours, do not disturb, night mode, split-flap quiet, Vestaboard silence] +--- + +# Silence Schedule + +The silence schedule feature lets you set quiet hours for your board, preventing any updates during specified time periods. This is useful for nighttime hours or any time you don't want the board making noise. + +## Overview + +Split-flap displays can be noisy when updating. The silence schedule ensures your board stays quiet during hours when you don't want it flipping characters, like when you're sleeping. + +## Configuring Silence Hours + + + +### Via the Web UI + +1. Open the FiestaBoard Web UI at `http://localhost:4420` +2. Go to **Settings** → **Behavior** +3. Find the **Silence Schedule** section +4. Set your **Start Time** and **End Time** +5. Save your settings + +Settings save automatically a second after you stop typing. + +## Multiple Boards + +Silence settings are **per board**. If you run a Flagship in the living room +and a Note on your desk, each keeps its own quiet hours, its own silence mode, +and its own silence page — the Flagship can go quiet at 10 PM while the Note +stays live, and each can show a page sized for its own display. + +Pick the board you want with the board selector in the sidebar, then edit the +Silence Schedule card; you are editing that board's settings. + +A few things follow from this: + +- **A board you have not configured inherits the install-wide schedule.** That + is the schedule you had before you ever touched a second board, so upgrading + changes nothing: every existing board keeps the quiet hours it already had. + A board you add later starts out quiet during the same window rather than + flipping at 3 AM. +- **The silence page picker only offers pages that fit that board.** A 22×6 + Flagship page cannot be chosen for a 15×3 Note. +- **Single-board installs see no difference.** There is just the one board, so + its schedule is the schedule. +- **Manual sends respect the target board's window.** Sending a message or a + page from the Web UI, the API, or Home Assistant is blocked while *that* + board is silenced, so a 2 AM send cannot wake a board whose own quiet hours + say it should be asleep. +- **The Home Assistant `silence_mode` sensor reports the primary board.** Like + the other entities in that payload (active page, transition style), it + describes the primary board rather than the install as a whole. The same is + true of `GET /silence-status` when you do not name a board. + +## How It Works + +When the current time falls within the silence window, FiestaBoard chooses +how to handle the board based on the configured **silence mode**: + +| Mode | Behaviour | +| --- | --- | +| **Show "SNOOZING" message** (default) | The board is set to a clean `SNOOZING` message sized for your device (Flagship or Note). No other content is overlaid. | +| **Leave board unchanged** | The board is left exactly as it was — no further updates are sent until silence ends. | +| **Show a specific page** | A page you choose is rendered once when silence begins and frozen on the board. Template variables are not refreshed until silence ends. | + +The mode is chosen per board, and so is the page: the silence page is always +rendered at the size of the board it is being sent to. + +Regardless of which mode you pick: + +- The display service stops sending refresh updates to that board while it is silenced +- The web UI and API continue to function normally +- Scheduled pages are skipped during that board's silence hours + +When the silence window ends: + +- The display service resumes normal operation +- The currently scheduled (or default) page is sent to the board + +:::tip +The silence schedule uses your configured timezone (`TIMEZONE` in `.env`). Make sure this is set correctly for your location. +::: + +## Example Configurations + +| Goal | Start Time | End Time | +| --- | --- | --- | +| Nighttime quiet hours | `22:00` (10 PM) | `07:00` (7 AM) | +| Quiet outside office hours | `18:00` (6 PM) | `08:00` (8 AM) | + +:::caution +`SILENCE_SCHEDULE_START_TIME` and `SILENCE_SCHEDULE_END_TIME` appear in +`env.example`, but the display service does not read them — they are written +to a config key nothing consumes. Set your quiet hours in the Web UI (or via +`PUT /settings/silence-schedule`) instead. +::: + +## Next Steps + +- [Schedule Mode](/docs/features/schedule) - Set up time-based page rotation +- [Environment Variables](/docs/reference/environment-variables) - All configuration options diff --git a/versioned_docs/version-9.14/features/transitions.md b/versioned_docs/version-9.14/features/transitions.md new file mode 100644 index 0000000000..8f06f3e47f --- /dev/null +++ b/versioned_docs/version-9.14/features/transitions.md @@ -0,0 +1,146 @@ +--- +sidebar_position: 6 +description: "Animate how your FiestaBoard changes between messages — built-in Vestaboard flip strategies and beta transition plugins like typewriter, dissolve, and slot machine." +keywords: [FiestaBoard transitions, Vestaboard transition, flip animation, transition plugins, typewriter transition, dissolve transition, split-flap animation, Transition Lab] +--- + +# Transitions + +Transitions control how your board gets from the message it is showing now to the next one — a left-to-right wave, a dissolve, a typewriter reveal, or nothing at all. + +## Overview + +FiestaBoard has **two different kinds of transitions**, and they behave differently. Knowing which one you are looking at explains most of the surprises people run into. + +| | Built-in flip strategies | Transition plugins (beta) | +|---|---|---| +| **Who animates** | The board, via the Vestaboard Local API | FiestaBoard, by sending many frames | +| **Works on** | Local API connections only | Any board connection (Local or Cloud) | +| **Examples** | Wave, Drift, Curtain, Row, Diagonal, Random | Typewriter, Simple Dissolve, Slot Machine, Quiet Library | +| **Where to turn on** | Always available | **Settings → Advanced → Beta Features → Transition Plugins** | +| **Step Interval / Step Size** | Supported | Not used — plugins set their own pacing | +| **Stability** | Stable | Experimental; behavior and settings may change | + +## Built-in Flip Strategies + +Built-in strategies are a **Vestaboard Local API** feature. FiestaBoard sends the new message plus the name of a flip pattern through the API, and the board handles the animation. + +Set the default in **Settings → Behavior → Board Transitions**: + +| Name in the UI | API value | What it looks like | +|---|---|---| +| **None** | `null` | No animation — every character flips at once | +| **Wave** | `column` | Flips column by column, left to right | +| **Drift** | `reverse-column` | Flips column by column, right to left | +| **Curtain** | `edges-to-center` | Flips from both edges, meeting in the middle | +| **Row** | `row` | Flips row by row, top to bottom | +| **Diagonal** | `diagonal` | Flips in a diagonal wave, corner to corner | +| **Random** | `random` | Flips tiles in a random order | + +Two optional settings tune the built-in strategies: + +- **Step Interval (ms)** — delay between animation steps. Leave empty for the board default. +- **Step Size** — how many rows or columns animate at once. Leave empty for the board default. + +:::info +Built-in strategies only work over the **Local API**. If your board is configured for the Cloud API, FiestaBoard still sends your message, but the strategy is dropped and the board flips all tiles at once. Note-array boards also ignore built-in strategies and their step settings. +::: + +## Transition Plugins (Beta) + +Transition plugins are animated by FiestaBoard, not by the board. The plugin generates a sequence of complete board frames, and FiestaBoard sends them one after another. Because each frame is an ordinary board update, plugin transitions work on **any** connection type — including Cloud API boards that cannot use the built-in strategies. + +FiestaBoard ships with four: + +| Plugin | What it does | +|---|---| +| **Typewriter** | Reveals the target message left to right, character by character | +| **Simple Dissolve** | Replaces tiles in random order, dissolving the old message into the new one | +| **Slot Machine** | Spins each column through random characters, then locks columns left to right | +| **Quiet Library** | Updates word by word in small batches with long pauses, for the quietest possible refresh | + +### Turning the beta on + +1. Open **Settings → Advanced → Beta Features**. +2. Enable **Transition Plugins**. The change takes effect immediately — no restart. +3. **Transition Lab** appears in the sidebar, and installed transition plugins appear in every transition picker. + +There is no separate enable step for an individual transition plugin. Installing one is opting in: any installed transition plugin is selectable as soon as the beta flag is on. On the **Integrations** page they show up badged **Transition**, with no on/off toggle. + +:::warning This is genuinely experimental +Every frame of a plugin transition is a real send to your board, so plugin transitions are bound by the board's send pacing. On some connections and some boards an animation will look less smooth than the preview does, or will take longer than you expect. The plugin SDK is experimental and its APIs, settings, and behavior may change before general availability. +::: + +FiestaBoard enforces per-plugin caps so a runaway animation cannot take your board hostage — a maximum frame count (default 500), a maximum runtime (default 120 seconds), and a minimum interval between frames (default 50 ms). When a run hits a cap, FiestaBoard snaps the board to the final message and stops. + +## Choosing Where a Transition Applies + +There are two places to choose a transition, and the more specific one wins. + +### Global default + +**Settings → Behavior → Board Transitions** sets the default for every board update. When the beta is on, the picker lists the built-in strategies under **Built-in** and your installed transition plugins under **Transition Plugins**. + +### Per page + +The page editor's header toolbar has a **Transition** dropdown. It offers: + +- **Use global default** — the page inherits whatever is set in Settings +- Any built-in strategy +- Any installed transition plugin, when the beta is on + +Picking anything other than **Use global default** creates a page-level override that applies whenever that page is sent. Switching back to **Use global default** clears the override. + +### Resolution order + +1. The page's own transition, if it has one +2. Otherwise, the global default from Settings + +:::note +Transitions cannot currently be set per collection or per schedule entry. A scheduled page uses its own page-level transition if it has one, and the global default otherwise. +::: + +## Transition Lab + +The **Transition Lab** (`/transitions`) is a preview harness for plugin transitions. It appears in the sidebar only while the Transition Plugins beta is on. + +In the Lab you can: + +- Pick a transition plugin and two of your real pages to transition between +- Choose the device shape — Flagship, Note, or a note array +- Override the plugin's settings for a single run with a JSON config +- Play, pause, step frame by frame, or scrub the generated sequence + +Previews are generated in FiestaBoard and rendered in your browser. Nothing is sent to your board, so you can iterate as much as you like. + +When you want to see it on real tiles, **Test live on board** runs the transition once on your actual board, and **Restore board** snaps it back to its active page. (The normal display loop would restore it on its own too.) Live tests are blocked while silence mode is active or the board is paused, so a quiet-hours board stays quiet. + +## Troubleshooting + +### Nothing animates — the whole board just changes at once + +Most often this is a Cloud API board with a built-in strategy selected. Built-in strategies are a Local API feature. Either switch the board to the Local API (see [API Keys](/docs/setup/api-keys)), or use a transition plugin, which works on any connection. + +Note-array boards do not support built-in strategies at all. + +### No Transition Lab in the sidebar, and no plugins in the transition pickers + +Plugin transitions stay hidden until the beta is on. Enable **Settings → Advanced → Beta Features → Transition Plugins** — the Transition Lab and the plugin entries appear immediately. Built-in strategies do not need the beta flag; they are always listed in Settings and in the page editor's **Transition** dropdown. + +### A plugin transition looks choppy or slower than the preview + +The preview runs entirely in FiestaBoard; a live run sends every frame to the board and is limited by the board's send pacing. Increasing the plugin's frame interval usually looks better than fighting for a faster one. This is a known limitation of animating a physical split-flap over an API. + +### Step Interval and Step Size seem to do nothing + +They only apply to built-in strategies. Transition plugins pace themselves through their own settings — for example Typewriter's `chars_per_frame` and `frame_interval_ms`. + +### A transition endpoint returns 404 + +The `/transitions/*` API endpoints are gated behind the beta flag and respond `404` while it is off. They are part of the web UI's internal surface rather than the published API, so they appear in `/api/internal/openapi.json` rather than in the [API Endpoints](/docs/reference/api-endpoints) reference. + +## Next Steps + +- [Page Editor](/docs/features/page-editor) - Set a per-page transition while you build a page +- [Plugin Development Guide](/docs/development/plugin-guide) - Build your own transition plugin +- [API Endpoints](/docs/reference/api-endpoints) - The published REST API reference diff --git a/versioned_docs/version-9.14/features/updating.md b/versioned_docs/version-9.14/features/updating.md new file mode 100644 index 0000000000..a75bc6d203 --- /dev/null +++ b/versioned_docs/version-9.14/features/updating.md @@ -0,0 +1,199 @@ +--- +sidebar_position: 10 +description: "FiestaBoard 5.0 introduces one-click in-app updates. Learn how Update Now works, how to opt in on Docker installs, and how auto-update works on FiestaPi." +keywords: [FiestaBoard update, in-app update, self-update, Update Now, fiestaupdater, FiestaPi update] +--- + +# In-App Updates + +Starting in FiestaBoard **5.0**, you can update FiestaBoard directly from the web UI — no terminal required. + +## The Update Now button + +When a new version is available, a banner appears in **Settings → System**. Click **Update Now**, confirm, and FiestaBoard handles the rest: + +1. The `fiestaupdater` sidecar pulls the new Docker image in the background +2. FiestaBoard restarts with the new image +3. A full-screen overlay polls the API every 2 seconds +4. When the new version is detected, the page reloads automatically + +The whole process takes about a minute on a fast connection, a bit longer on a Raspberry Pi. + +## Update check interval + +In **Settings → System**, there's a **Check for updates** dropdown that +controls how often FiestaBoard looks for a new release in the background: + +| Option | Default for | Notes | +|---|---|---| +| **Every day** | FiestaPi | Same cadence as before; you'll hear about updates the day they ship. | +| **Every week** | Docker / manual | New default. Stay current without daily noise. | +| **Every month** | — | Quiet option that still nudges you periodically. | +| **Manual only** | — | No background checks; click the refresh button on the System card to check on demand. | + +When a check finds a newer version, the "Update Available" banner appears +on Settings → System. Picking the matching cadence for your install means +you don't have to remember to open Settings to discover an update. + +You can change it any time; the running scheduler picks up the new value +within an hour without needing a restart. + +## How it works (technical) + +FiestaBoard uses a small companion container called `fiestaupdater`. It sits on the same Docker Compose internal network as FiestaBoard and has access to the Docker socket — FiestaBoard itself does not. + +When you click Update Now, the web UI POSTs to the FiestaBoard API, which calls the updater over the internal network. The updater runs `docker compose pull && docker compose up -d` against your compose file. Because it's a separate process, FiestaBoard can update itself without getting cut off mid-response. + +**See also:** [FiestaUpdater reference](/docs/deployment/fiestaupdater) for the full sidecar architecture, HTTP API, and security model. + +## Opting in on Docker installs + +The updater is opt-in for Docker and manual installs (it's already on for FiestaPi). + +**Via the install script (easiest):** Run `./scripts/install.sh` (or `install.ps1` on Windows) — it asks about this and configures everything. + +**Manually:** Edit your `.env` file: + +```bash +COMPOSE_PROFILES=fiestaupdater +FIESTAUPDATER_TOKEN=<64-hex-char random string> +``` + +Generate a token: + +```bash +head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' +``` + +Then restart: + +```bash +docker compose up -d +``` + +The Update Now button will appear in Settings → System once the sidecar is healthy. + +## Security + +- The updater is only reachable on the Docker Compose internal network — it's never published on a host port +- All requests require a bearer token (`FIESTAUPDATER_TOKEN`), unique to your install +- The sidecar can only restart the service listed in `FIESTAUPDATER_SERVICE` — no arbitrary container access + +If you prefer not to use the Docker socket at all, leave the `fiestaupdater` profile off and update manually instead. + +## Updating manually (always works) + +The Update Now button is a convenience shortcut. You can always update from the terminal: + +```bash +# For Docker Hub installs — cd to the SAME folder you originally ran +# `docker compose up -d` from. That's where your ./data folder lives. +cd ~/fiestaboard +docker compose -f docker-compose.hub.yml pull +docker compose -f docker-compose.hub.yml up -d + +# For FiestaPi (SSH in first) +ssh fiesta@fiestapi.local +cd /opt/fiestaboard +docker compose pull && docker compose up -d +``` + +:::caution Always `cd` first on Docker installs +The compose file uses a relative bind mount (`./data:/app/data`), so persistence depends on your current shell directory. If you run `docker compose pull && up -d` from a different folder than your original install, FiestaBoard will come up with empty settings — your data isn't deleted, but Docker is mounting a different (empty) folder. See [Troubleshooting → Settings or board credentials are gone after an update](/docs/troubleshooting#settings-or-board-credentials-are-gone-after-an-update) if this has happened. +::: + +## The beta channel + +Beta builds come from the `next` branch and carry changes that have not reached +a stable release yet. They are published on every merge, so a beta is newer than +stable but has had far less time in front of real boards. + +:::caution Betas can break, and going back is not free +A beta may migrate your settings and pages to a format stable does not +understand. FiestaBoard refuses to read such a file rather than misinterpret it, +so returning to stable means restoring a backup. **Take one before you opt in:** +Settings → System → Backup → Export. +::: + +### FiestaPi + +Settings → System → **Release channel** → **Join the beta**. + +A backup is taken automatically before the switch, and FiestaBoard restarts onto +the beta build. From then on **Update Now** keeps you on betas — it moves you +from one beta to the next, not back to stable. + +Joining is one-way on a Pi for now; see [Going back to stable](#going-back-to-stable) +before you opt in. + +The choice survives a reboot. A Pi re-pulls its images on every boot, which would +otherwise put you back on stable, so FiestaBoard re-applies your channel at +startup if the build that came up does not match it. + +### Docker installs + +You already control which build you run — it is the image tag in your compose +file. Edit `docker-compose.hub.yml`: + +```yaml +services: + fiestaboard: + image: fiestaboard/fiestaboard:beta # was :latest +``` + +Then, from the folder you originally installed in: + +```bash +cd ~/fiestaboard +docker compose -f docker-compose.hub.yml pull +docker compose -f docker-compose.hub.yml up -d +``` + +Two tags are published: + +| Tag | Use it when | +| --- | --- | +| `beta` | You want the newest beta, and want Update Now to keep you on betas | +| `9.0.0-beta.12` | You want to stay on one exact build, or you are filing a bug | + +The in-app **Update Now** button pulls whatever tag your compose file names, so +once you are on `:beta` it keeps you on betas. Nothing else to configure. + +### Home Assistant + +Not available. The HA Supervisor owns updating for add-ons — FiestaBoard cannot +change the image it runs, and the Release channel card will tell you so rather +than offering a button that cannot work. Supporting betas there means shipping a +separate add-on entry, which is not planned yet. + +### Going back to stable + +**Docker** — set the tag back to `:latest`, pull, bring the stack up again, then +restore the backup you took before opting in. + +If you skipped that backup and the app reports a schema it cannot read, your data +is still on disk and intact. The newer FiestaBoard can still read it, so going +back to `:beta` gets you running again — export a backup, then switch. + +**FiestaPi** — there is no in-app way back yet. + +:::warning A FiestaPi cannot leave the beta from the app +Joining is currently one-way on a Pi. The control to return to stable has not +shipped, so treat joining as a decision to stay on beta for now. If that is not +what you want, wait — it is being worked on. +::: + +## Troubleshooting + +**The Update Now button isn't showing** — Check the status panel just below the button area in Settings → System. It will say whether the updater sidecar is reachable. Most common cause: `COMPOSE_PROFILES=fiestaupdater` isn't set in `.env`, or the sidecar container isn't running. + +**Update started but the page never came back** — Open `http://localhost:4420` on the Docker host, or `http://fiestapi.local:4420` on a FiestaPi, again after a minute. From another device on a standard Docker install, use the host's IP address. If it's still down, check logs: `docker logs fiestaboard`. As a last resort, `docker compose up -d` will bring you back online. + +**I joined the beta but came back on stable** — The switch works by pointing +the image name in your compose file at the beta build, and an out-of-date +updater sidecar can undo that when it recreates the container. FiestaBoard re-applies your channel at +startup, so the box usually arrives on the beta a minute later after an extra +restart. To stop it happening: reboot a FiestaPi to pick up the current sidecar, +or on Docker run `docker compose pull fiestaupdater && docker compose up -d`. + +**I want to roll back** — Pin the previous image tag in `docker-compose.yml`, then `docker compose up -d`. Automated rollback is planned for a future release. diff --git a/versioned_docs/version-9.14/intro.md b/versioned_docs/version-9.14/intro.md new file mode 100644 index 0000000000..4016c99009 --- /dev/null +++ b/versioned_docs/version-9.14/intro.md @@ -0,0 +1,123 @@ +--- +sidebar_position: 1 +description: "FiestaBoard is free, open-source software for Vestaboard and split-flap displays. 50+ plugins including weather, stocks, sports, transit, and optional AI-assisted page drafting for your Vestaboard Flagship or Note." +keywords: [FiestaBoard, split-flap display, split-flap display software, Vestaboard, Vestaboard software, Vestaboard app, Vestaboard dashboard, Vestaboard open source, best software for Vestaboard, smart dashboard, live display, open source, Vestaboard AI, AI page generation] +--- + +# Welcome to FiestaBoard + +FiestaBoard is free, open-source software for Vestaboard and split-flap displays. It connects to your board and lets you control what it shows through a web interface with plugins, a visual editor, and scheduling. Compatible with Vestaboard Flagship (22x6) and Note (15x3). You bring the board, you bring the API keys for the services you care about, and FiestaBoard handles pulling data from those services and formatting it for your display. + +## What Can FiestaBoard Do? + +Here's a quick look at what your board can show once FiestaBoard is running: + +| Category | Examples | +|----------|---------| +| **Weather & Environment** | Temperature, UV index, precipitation, air quality, fog, surf conditions | +| **Finance** | Stock prices with color-coded change indicators | +| **Transit & Travel** | Muni arrivals, traffic commute times, ferry schedules, bike share availability | +| **Sports & Entertainment** | NFL/NBA/NHL/Soccer scores, Star Trek quotes, currently playing music | +| **Home** | Smart home status via Home Assistant, guest WiFi credentials | +| **Fun & Visual** | Disney park wait times, sun art, visual clock, stardate | + +FiestaBoard has a catalog of **50+ plugins**, and many of them work without any API key at all. You install them from the [plugin registry](/plugins) — a curated list maintained in the FiestaBoard repository — or from any public git repository. A small set ships bundled in the repo; the rest are pulled in on demand when you enable them. + +## How It Works + +1. **Install FiestaBoard** on any computer with Docker (your laptop, a Raspberry Pi, a home server) +2. **Connect your board** by entering your board's API key in the web UI. FiestaBoard talks to the board only through Vestaboard's official [Local API](/docs/setup/api-keys) or [Cloud API](/docs/setup/cloud-api) — the same APIs any developer can use. +3. **Enable plugins** to pull in the data you care about (weather, stocks, transit, etc.) +4. **Create pages** using the visual editor to design exactly what your board displays — or describe a page in natural language and let an LLM draft it for you with the optional [Gen AI page drafts](/docs/setup/ai-providers) feature (bring your own API key) +5. **Set a schedule** so different pages show at different times of day (optional) + +Everything after the initial install is done through a web interface at **http://localhost:4420** - no config files to edit, no code to write. + +## What You'll Need + +- **A Vestaboard** (Flagship or Note) or compatible split-flap display, already set up and working +- **Your board's API key** ([how to find it](/docs/setup/api-keys)) +- **A Raspberry Pi** (recommended — easiest path), **or** a computer with Docker installed, to run the FiestaBoard software. This is a separate device on your network; nothing gets installed on the board itself. + +That's it. No other API keys or configuration are needed to get started. Plugins that connect to external services (weather, traffic, etc.) can be enabled and configured later through the web UI. + +## The Easiest Way: Flash a Raspberry Pi + +:::tip 🥇 Recommended for everyone +The simplest, most reliable way to run FiestaBoard — for technical and non-technical users alike — is to flash a Raspberry Pi with our pre-built **FiestaPi** image using **[Raspberry Pi Imager](https://www.raspberrypi.com/software/)**. + +**No Docker setup. No command line. No config files. Self-updating with one click.** + +You only need: a Raspberry Pi (3B or newer), a microSD card, and 5 minutes. + +**[→ FiestaPi Quick Start](/docs/setup/raspberry-pi)** — flash the image, boot the Pi, open a browser. +::: + +If you don't have a Pi yet, any model from the Raspberry Pi 3B onwards works. The Pi 4 and Pi 5 are great choices, and even the inexpensive **[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/)** can run FiestaBoard. + +## Other Ways to Run FiestaBoard + +Don't have a Pi, or already have a home server? FiestaBoard runs anywhere Docker runs — and if you already use Home Assistant OS or Supervised, you can install it from the HA Add-on Store. + +
+
+ +### I'm new to all this + +Never used Docker or the command line? No problem. + +**[Beginner's Guide](/docs/setup/beginners-guide)** walks you through every step with clear instructions — including the easy Pi-flash path and a Docker fallback. + +
+
+ +### I'm comfortable with Docker + +Know your way around `docker compose`? Run FiestaBoard on a laptop, NAS, or home server. + +**[Docker Quick Start](/docs/setup/quick-start)** gets you running in under 5 minutes. + +
+
+ +### I run Home Assistant 🏠 + +On Home Assistant OS or Supervised? Install FiestaBoard from the **HA Add-on Store** with one click — Ingress, MQTT auto-discovery, and HA backups all wired up. + +**[Home Assistant Add-on](/docs/setup/home-assistant-addon)** — currently in beta, feedback welcome via [GitHub issues](https://github.com/Fiestaboard/FiestaBoard-Home-Assistant-App/issues) or [Discord](https://discord.gg/2GAqKnRF6h). + +
+
+ +--- + +### Already running? Here's what to do next + +If you've already got FiestaBoard installed and running, check out **[Your First 10 Minutes](/docs/setup/first-10-minutes)** to learn how to create your first page, enable your first plugin, and set up a schedule. + +## All Documentation + +| Section | What's Covered | +|---------|---------------| +| **[FiestaPi Quick Start](/docs/setup/raspberry-pi)** | Flash a Pi image and be running in minutes | +| **[Quick Start](/docs/setup/quick-start)** | Installation and first run (Docker) | +| **[Home Assistant Add-on](/docs/setup/home-assistant-addon)** | Install from the HA Add-on Store (beta) — Ingress, MQTT auto-discovery, HA backups | +| **[Beginner's Guide](/docs/setup/beginners-guide)** | Step-by-step for non-technical users | +| **[Your First 10 Minutes](/docs/setup/first-10-minutes)** | What to do right after setup | +| **[Plugin Directory](/plugins)** | All 50+ plugins and what they do | +| **[Page Editor](/docs/features/page-editor)** | Creating and editing board content | +| **[AI Page Drafts](/docs/setup/ai-providers)** | Optional: bring your own LLM to draft pages from a prompt | +| **[Schedule Mode](/docs/features/schedule)** | Automating when pages display | +| **[Silence Schedule](/docs/features/silence-schedule)** | Setting quiet hours for your board | +| **[In-App Updates](/docs/features/updating)** | One-click updates from the Settings UI | +| **[Raspberry Pi](/docs/deployment/raspberry-pi)** | Always-on deployment on a Pi | +| **[V3 Migration](/docs/setup/v3-migration)** | Upgrading from FiestaBoard V2 (external plugins) | +| **[V2 Migration](/docs/setup/v2-migration)** | Upgrading from FiestaBoard V1 | +| **[Troubleshooting](/docs/troubleshooting)** | Common issues and solutions | +| **[Plugin Development](/docs/development/plugin-guide)** | Creating your own plugins | + + + +## About Vestaboard + +FiestaBoard is an independent, open-source project. It isn't affiliated with, sponsored by, or endorsed by Vestaboard, Inc. "Vestaboard" is a trademark of Vestaboard, Inc. FiestaBoard is software that sends messages to compatible boards through Vestaboard's official APIs. For questions about the board hardware itself, see [Vestaboard's own documentation](https://docs.vestaboard.com/). diff --git a/versioned_docs/version-9.14/reference/_category_.json b/versioned_docs/version-9.14/reference/_category_.json new file mode 100644 index 0000000000..477b8ea9f6 --- /dev/null +++ b/versioned_docs/version-9.14/reference/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Reference", + "position": 5, + "link": { + "type": "generated-index", + "description": "Technical reference for FiestaBoard character codes, colors, API endpoints, and configuration." + } +} diff --git a/versioned_docs/version-9.14/reference/accessibility.md b/versioned_docs/version-9.14/reference/accessibility.md new file mode 100644 index 0000000000..bd11bc6b36 --- /dev/null +++ b/versioned_docs/version-9.14/reference/accessibility.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 5 +description: "FiestaBoard's commitment to WCAG 2.2 Level AA accessibility compliance." +keywords: [FiestaBoard, accessibility, WCAG, WCAG 2.2, AA, a11y, assistive technology] +--- + +# Accessibility + +FiestaBoard aims to conform to the [Web Content Accessibility Guidelines (WCAG) 2.2](https://www.w3.org/TR/WCAG22/) at the **AA** level. AA is the standard most public sector and commercial sites target, and it guides every decision we make about the web UI. + +## What This Means + +WCAG 2.2 AA covers a wide range of requirements across four principles: + +- **Perceivable** — Content is presented in ways that all users can perceive, including text alternatives for images, captions, and sufficient contrast (minimum 4.5:1 ratio for normal text and 3:1 for large text and UI components). +- **Operable** — The interface is fully navigable by keyboard, provides visible focus indicators, and avoids content that could cause seizures or motion sickness. +- **Understandable** — Text is readable and predictable. Forms include labels, instructions, and helpful error messages. +- **Robust** — Markup is valid and compatible with current and future assistive technologies. + +## Contributing Accessible Code + +If you are contributing to FiestaBoard, please keep these guidelines in mind: + +- Use **semantic HTML** elements (`