🏡 index : ~doyle/shalom.git

author Jordan Doyle <jordan@doyle.la> 2023-10-31 19:45:32.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-10-31 19:50:03.0 +00:00:00
commit
32cf1a03ab5725b1efd5cba0993c75d5e388cfa6 [patch]
tree
d27be0c65054c5661bbed1e874b13c385c4da972
parent
c89f6fa1059502d03ae7d7e77be7045422f8bc90
download
32cf1a03ab5725b1efd5cba0993c75d5e388cfa6.tar.gz

Read current weather information from home assistant



Diff

 Cargo.lock                           |  29 +++-
 assets/icons/README.md               |   1 +-
 assets/icons/clear-day.svg           |   1 +-
 assets/icons/clear-night.svg         |   1 +-
 assets/icons/cloud.svg               |   1 +-
 assets/icons/extreme-rain.svg        |   1 +-
 assets/icons/fog.svg                 |   1 +-
 assets/icons/hail.svg                |   1 +-
 assets/icons/partly-cloudy-day.svg   |   1 +-
 assets/icons/partly-cloudy-night.svg |   1 +-
 assets/icons/rain.svg                |   1 +-
 assets/icons/snow.svg                |   1 +-
 assets/icons/thunderstorms-rain.svg  |   1 +-
 assets/icons/thunderstorms.svg       |   1 +-
 assets/icons/wind.svg                |   1 +-
 shalom/Cargo.toml                    |   1 +-
 shalom/src/hass_client.rs            | 303 ++++++++++++++++++++++++++----------
 shalom/src/oracle.rs                 |  57 ++++++-
 shalom/src/pages/omni.rs             |  22 +--
 shalom/src/theme.rs                  |  50 ++++--
 shalom/src/widgets/cards/mod.rs      |   1 +-
 shalom/src/widgets/cards/weather.rs  | 173 +++++++++++++++++++++-
 shalom/src/widgets/mod.rs            |   1 +-
 23 files changed, 545 insertions(+), 106 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 5a73ab8..c2e1179 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2326,6 +2326,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"

[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"

[[package]]
name = "rustybuzz"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2452,6 +2458,7 @@ dependencies = [
 "once_cell",
 "serde",
 "serde_json",
 "strum",
 "time",
 "tokio",
 "tokio-tungstenite",
@@ -2618,6 +2625,28 @@ dependencies = [
]

[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
 "strum_macros",
]

[[package]]
name = "strum_macros"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "rustversion",
 "syn 2.0.38",
]

[[package]]
name = "svg_fmt"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/assets/icons/README.md b/assets/icons/README.md
index 2aa9bfb..ad6e55f 100644
--- a/assets/icons/README.md
+++ b/assets/icons/README.md
@@ -1,3 +1,4 @@
# Resources

- https://heroicons.com/
- https://github.com/basmilius/weather-icons
diff --git a/assets/icons/clear-day.svg b/assets/icons/clear-day.svg
new file mode 100644
index 0000000..bd486fa
--- /dev/null
+++ b/assets/icons/clear-day.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="150" x2="234" y1="119.2" y2="264.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fbbf24"/><stop offset=".5" stop-color="#fbbf24"/><stop offset="1" stop-color="#f59e0b"/></linearGradient><symbol id="b" viewBox="0 0 384 384"><circle cx="192" cy="192" r="84" fill="url(#a)" stroke="#f8af18" stroke-miterlimit="10" stroke-width="6"/><path fill="none" stroke="#fbbf24" stroke-linecap="round" stroke-miterlimit="10" stroke-width="24" d="M192 61.7V12m0 360v-49.7m92.2-222.5 35-35M64.8 319.2l35.1-35.1m0-184.4-35-35m254.5 254.5-35.1-35.1M61.7 192H12m360 0h-49.7"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="rotate" values="0 192 192; 45 192 192"/></path></symbol></defs><use xlink:href="#b" width="384" height="384" transform="translate(64 64)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/clear-night.svg b/assets/icons/clear-night.svg
new file mode 100644
index 0000000..5f97fac
--- /dev/null
+++ b/assets/icons/clear-night.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="54.3" x2="187.2" y1="29" y2="259.1" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#86c3db"/><stop offset=".5" stop-color="#86c3db"/><stop offset="1" stop-color="#5eafcf"/></linearGradient><symbol id="b" overflow="visible" viewBox="0 0 270 270"><path fill="url(#a)" stroke="#72b9d5" stroke-linecap="round" stroke-linejoin="round" stroke-width="6" d="M252.3 168.6A133.4 133.4 0 01118 36.2 130.5 130.5 0 01122.5 3 133 133 0 003 134.6C3 207.7 63 267 137.2 267c62.5 0 114.8-42.2 129.8-99.2a135.6 135.6 0 01-14.8.8Z"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="rotate" values="-15 135 135; 9 135 135; -15 135 135"/></path></symbol></defs><use xlink:href="#b" width="270" height="270" transform="translate(121 121)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/cloud.svg b/assets/icons/cloud.svg
new file mode 100644
index 0000000..0f19f52
--- /dev/null
+++ b/assets/icons/cloud.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><symbol id="b" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol></defs><use xlink:href="#b" width="350" height="222" transform="translate(81 145)"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="translate" values="-18 0; 18 0; -18 0"/></use></svg>
\ No newline at end of file
diff --git a/assets/icons/extreme-rain.svg b/assets/icons/extreme-rain.svg
new file mode 100644
index 0000000..f1ffc4b
--- /dev/null
+++ b/assets/icons/extreme-rain.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="52.7" x2="133.4" y1="9.6" y2="149.3" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9ca3af"/><stop offset=".5" stop-color="#9ca3af"/><stop offset="1" stop-color="#6b7280"/></linearGradient><linearGradient id="b" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6b7280"/><stop offset=".5" stop-color="#6b7280"/><stop offset="1" stop-color="#4b5563"/></linearGradient><linearGradient id="c" x1="1381.3" x2="1399.5" y1="-1144.7" y2="-1097.4" gradientTransform="rotate(-9 8002.567 8233.063)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0b65ed"/><stop offset=".5" stop-color="#0a5ad4"/><stop offset="1" stop-color="#0950bc"/></linearGradient><linearGradient xlink:href="#c" id="d" x1="1436.7" x2="1454.9" y1="-1137" y2="-1089.7" gradientTransform="rotate(-9 8009.537 8233.037)"/><linearGradient xlink:href="#c" id="e" x1="1492.1" x2="1510.3" y1="-1129.3" y2="-1082.1" gradientTransform="rotate(-9 8016.566 8233.078)"/><symbol id="g" viewBox="0 0 200.3 126.1"><path fill="url(#a)" stroke="#848b98" stroke-miterlimit="10" d="M.5 93.2a32.4 32.4 0 0032.4 32.4h129.8v-.1l2.3.1a34.8 34.8 0 006.5-68.9 32.4 32.4 0 00-48.5-33 48.6 48.6 0 00-88.6 37.1h-1.5A32.4 32.4 0 00.5 93.1Z"/></symbol><symbol id="h" viewBox="0 0 350 222"><path fill="url(#b)" stroke="#5b6472" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="f" overflow="visible" viewBox="0 0 398 222"><use xlink:href="#g" width="200.3" height="126.1" transform="translate(198 27)"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="translate" values="-9 0; 9 0; -9 0"/></use><use xlink:href="#h" width="350" height="222"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="translate" values="-18 0; 18 0; -18 0"/></use></symbol><symbol id="i" overflow="visible" viewBox="0 0 129 57"><path fill="url(#c)" stroke="#0a5ad4" stroke-miterlimit="10" d="M8.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x1" additive="sum" attributeName="transform" begin="0s; x1.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y1" attributeName="opacity" begin="0s; y1.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path><path fill="url(#d)" stroke="#0a5ad4" stroke-miterlimit="10" d="M64.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x2" additive="sum" attributeName="transform" begin=".33s; x2.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y2" attributeName="opacity" begin=".33s; y2.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path><path fill="url(#e)" stroke="#0a5ad4" stroke-miterlimit="10" d="M120.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x3" additive="sum" attributeName="transform" begin="-.33s; x3.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y3" attributeName="opacity" begin="-.33s; y3.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path></symbol></defs><use xlink:href="#f" width="398" height="222" transform="translate(68.84 145)"/><use xlink:href="#i" width="129" height="57" transform="translate(191.5 343.5)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/fog.svg b/assets/icons/fog.svg
new file mode 100644
index 0000000..09d9dff
--- /dev/null
+++ b/assets/icons/fog.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="96" x2="168" y1="-2.4" y2="122.3" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d4d7dd"/><stop offset=".5" stop-color="#d4d7dd"/><stop offset="1" stop-color="#bec1c6"/></linearGradient><linearGradient xlink:href="#b" id="c" x2="168" y1="-50.4" y2="74.3"/><symbol id="d" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="e" overflow="visible" viewBox="0 0 264 72"><path fill="none" stroke="url(#b)" stroke-linecap="round" stroke-miterlimit="10" stroke-width="24" d="M12 60h240"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="translate" values="-24 0; 24 0; -24 0"/></path><path fill="none" stroke="url(#c)" stroke-linecap="round" stroke-miterlimit="10" stroke-width="24" d="M12 12h240"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="translate" values="24 0; -24 0; 24 0"/></path></symbol></defs><use xlink:href="#d" width="350" height="222" transform="translate(81 145)"/><use xlink:href="#e" width="264" height="72" transform="translate(124 402)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/hail.svg b/assets/icons/hail.svg
new file mode 100644
index 0000000..f58baac
--- /dev/null
+++ b/assets/icons/hail.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="6.5" x2="18.5" y1="2.1" y2="22.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#86c3db"/><stop offset=".5" stop-color="#86c3db"/><stop offset="1" stop-color="#5eafcf"/></linearGradient><linearGradient xlink:href="#b" id="c" x1="62.5" x2="74.5" y1="2.1" y2="22.9"/><linearGradient xlink:href="#b" id="d" x1="118.5" x2="130.5" y1="2.1" y2="22.9"/><symbol id="e" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="f" overflow="visible" viewBox="0 0 137 25"><path fill="url(#b)" stroke="#86c3db" stroke-miterlimit="10" d="M12.5.5a12 12 0 1012 12 12 12 0 00-12-12Z" opacity="0"><animateTransform id="x1" additive="sum" attributeName="transform" begin="0s; x1.end+.42s" dur=".58s" keyTimes="0; .71; 1" type="translate" values="0 -46; 0 86; -18 74"/><animate id="y1" attributeName="opacity" begin="0s; y1.end+.42s" dur=".58s" keyTimes="0; .14; .71; 1" values="0; 1; 1; 0"/></path><path fill="url(#c)" stroke="#86c3db" stroke-miterlimit="10" d="M68.5.5a12 12 0 1012 12 12 12 0 00-12-12Z" opacity="0"><animateTransform id="x2" additive="sum" attributeName="transform" begin=".67s; x2.end+.42s" dur=".58s" keyTimes="0; .71; 1" type="translate" values="0 -46; 0 86; 0 74"/><animate id="y2" attributeName="opacity" begin=".67s; y2.end+.42s" dur=".58s" keyTimes="0; .14; .71; 1" values="0; 1; 1; 0"/></path><path fill="url(#d)" stroke="#86c3db" stroke-miterlimit="10" d="M124.5.5a12 12 0 1012 12 12 12 0 00-12-12Z" opacity="0"><animateTransform id="x3" additive="sum" attributeName="transform" begin=".33s; x3.end+.42s" dur=".58s" keyTimes="0; .71; 1" type="translate" values="0 -46; 0 86; 18 74"/><animate id="y3" attributeName="opacity" begin=".33s; y3.end+.42s" dur=".58s" keyTimes="0; .14; .71; 1" values="0; 1; 1; 0"/></path></symbol></defs><use xlink:href="#e" width="350" height="222" transform="translate(81 145)"/><use xlink:href="#f" width="137" height="25" transform="translate(187.5 349.5)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/partly-cloudy-day.svg b/assets/icons/partly-cloudy-day.svg
new file mode 100644
index 0000000..cdff303
--- /dev/null
+++ b/assets/icons/partly-cloudy-day.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><symbol id="d" viewBox="0 0 196 196"><circle cx="98" cy="98" r="40" fill="url(#b)" stroke="#f8af18" stroke-miterlimit="10" stroke-width="4"/><path fill="none" stroke="#fbbf24" stroke-linecap="round" stroke-miterlimit="10" stroke-width="12" d="M98 31.4V6m0 184v-25.4M145.1 51l18-17.9M33 163l18-17.9M51 51 33 33m130.1 130.1-18-18M6 98h25.4M190 98h-25.4"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="rotate" values="0 98 98; 45 98 98"/></path></symbol><symbol id="e" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="c" viewBox="0 0 363 258"><use xlink:href="#d" width="196" height="196"/><use xlink:href="#e" width="350" height="222" transform="translate(13 36)"/></symbol><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="78" x2="118" y1="63.4" y2="132.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fbbf24"/><stop offset=".5" stop-color="#fbbf24"/><stop offset="1" stop-color="#f59e0b"/></linearGradient></defs><use xlink:href="#c" width="363" height="258" transform="translate(68 109)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/partly-cloudy-night.svg b/assets/icons/partly-cloudy-night.svg
new file mode 100644
index 0000000..1cfeb3a
--- /dev/null
+++ b/assets/icons/partly-cloudy-night.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><symbol id="d" viewBox="0 0 172 172"><path fill="url(#b)" stroke="#72b9d5" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M160.6 107.4a84.8 84.8 0 01-85.4-84.3A83.3 83.3 0 0178 2 84.7 84.7 0 002 85.7 84.8 84.8 0 0087.4 170a85.2 85.2 0 0082.6-63.1 88 88 0 01-9.4.5Z"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="rotate" values="-15 86 86; 9 86 86; -15 86 86"/></path></symbol><symbol id="e" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="c" viewBox="0 0 351 246"><use xlink:href="#d" width="172" height="172"/><use xlink:href="#e" width="350" height="222" transform="translate(1 24)"/></symbol><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="34.7" x2="119.2" y1="18.6" y2="165" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#86c3db"/><stop offset=".5" stop-color="#86c3db"/><stop offset="1" stop-color="#5eafcf"/></linearGradient></defs><use xlink:href="#c" width="351" height="246" transform="translate(80 121)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/rain.svg b/assets/icons/rain.svg
new file mode 100644
index 0000000..52912e9
--- /dev/null
+++ b/assets/icons/rain.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="1381.3" x2="1399.5" y1="-1144.7" y2="-1097.4" gradientTransform="rotate(-9 8002.567 8233.063)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0b65ed"/><stop offset=".5" stop-color="#0a5ad4"/><stop offset="1" stop-color="#0950bc"/></linearGradient><linearGradient xlink:href="#b" id="c" x1="1436.7" x2="1454.9" y1="-1137" y2="-1089.7" gradientTransform="rotate(-9 8009.537 8233.037)"/><linearGradient xlink:href="#b" id="d" x1="1492.1" x2="1510.3" y1="-1129.3" y2="-1082.1" gradientTransform="rotate(-9 8016.566 8233.078)"/><symbol id="e" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="f" overflow="visible" viewBox="0 0 129 57"><path fill="url(#b)" stroke="#0a5ad4" stroke-miterlimit="10" d="M8.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x1" additive="sum" attributeName="transform" begin="0s; x1.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y1" attributeName="opacity" begin="0s; y1.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path><path fill="url(#c)" stroke="#0a5ad4" stroke-miterlimit="10" d="M64.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x2" additive="sum" attributeName="transform" begin=".33s; x2.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y2" attributeName="opacity" begin=".33s; y2.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path><path fill="url(#d)" stroke="#0a5ad4" stroke-miterlimit="10" d="M120.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x3" additive="sum" attributeName="transform" begin="-.33s; x3.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y3" attributeName="opacity" begin="-.33s; y3.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path></symbol></defs><use xlink:href="#e" width="350" height="222" transform="translate(81 145)"/><use xlink:href="#f" width="129" height="57" transform="translate(191.5 343.5)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/snow.svg b/assets/icons/snow.svg
new file mode 100644
index 0000000..bf96c7b
--- /dev/null
+++ b/assets/icons/snow.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="11.4" x2="32.8" y1="5.9" y2="43.1" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#86c3db"/><stop offset=".5" stop-color="#86c3db"/><stop offset="1" stop-color="#5eafcf"/></linearGradient><linearGradient xlink:href="#b" id="c" x1="67.4" x2="88.8" y1="5.9" y2="43.1"/><linearGradient xlink:href="#b" id="d" x1="123.4" x2="144.8" y1="5.9" y2="43.1"/><symbol id="e" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="f" overflow="visible" viewBox="0 0 156.2 49"><g><path fill="url(#b)" stroke="#86c3db" stroke-miterlimit="10" d="m41.7 31-5.8-3.3a13.7 13.7 0 000-6.5l5.8-3.2a4 4 0 001.5-5.5 4 4 0 00-5.6-1.5l-5.8 3.3a13.6 13.6 0 00-2.6-2 13.8 13.8 0 00-3-1.3V4.5a4 4 0 00-8.1 0v6.6a14.3 14.3 0 00-5.7 3.2L6.6 11A4 4 0 001 12.5 4 4 0 002.5 18l5.8 3.3a13.7 13.7 0 000 6.5L2.5 31A4 4 0 001 36.5a4 4 0 003.5 2 4 4 0 002-.5l5.8-3.3a13.6 13.6 0 002.6 2 13.8 13.8 0 003 1.2v6.6a4 4 0 008.2 0v-6.6a14.2 14.2 0 005.6-3.2l6 3.3a4 4 0 002 .5 4 4 0 003.4-2 4 4 0 00-1.4-5.5ZM19 29.7a6 6 0 01-2.3-8.2 6.1 6.1 0 015.3-3 6.2 6.2 0 013 .8 6 6 0 012.2 8.2 6.1 6.1 0 01-8.2 2.2Z" opacity="0"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="rotate" values="0 24 24; 360 24 24"/><animate id="t1" attributeName="opacity" begin="0s; t1.end+1s" dur="2s" keyTimes="0; .17; .83; 1" values="0; 1; 1; 0"/></path><animateTransform id="s1" additive="sum" attributeName="transform" begin="0s; s1.end+1s" dur="2s" type="translate" values="0 -36; 0 92;"/></g><g><path fill="url(#c)" stroke="#86c3db" stroke-miterlimit="10" d="m97.7 31-5.8-3.3a13.7 13.7 0 000-6.5l5.8-3.2a4 4 0 001.5-5.5 4 4 0 00-5.6-1.5l-5.8 3.3a13.6 13.6 0 00-2.6-2 13.8 13.8 0 00-3-1.3V4.5a4 4 0 00-8.1 0v6.6a14.3 14.3 0 00-5.7 3.2L62.6 11a4 4 0 00-5.6 1.5 4 4 0 001.5 5.5l5.8 3.3a13.7 13.7 0 000 6.5L58.5 31a4 4 0 00-1.5 5.5 4 4 0 003.5 2 4 4 0 002-.5l5.8-3.3a13.6 13.6 0 002.7 2 13.8 13.8 0 003 1.2v6.6a4 4 0 008 0v-6.6a14.2 14.2 0 005.7-3.2l6 3.3a4 4 0 002 .5 4 4 0 003.4-2 4 4 0 00-1.4-5.5ZM75 29.7a6 6 0 01-2.3-8.2 6.1 6.1 0 015.3-3 6.2 6.2 0 013 .8 6 6 0 012.2 8.2 6.1 6.1 0 01-8.2 2.2Z" opacity="0"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="rotate" values="0 80 24; 360 80 24"/><animate id="t2" attributeName="opacity" begin="-.83s; t2.end+1s" dur="2s" keyTimes="0; .17; .83; 1" values="0; 1; 1; 0"/></path><animateTransform id="s2" additive="sum" attributeName="transform" begin="-.83s; s2.end+1s" dur="2s" type="translate" values="0 -36; 0 92;"/></g><g><path fill="url(#d)" stroke="#86c3db" stroke-miterlimit="10" d="m153.7 31-5.8-3.3a13.7 13.7 0 000-6.5l5.8-3.2a4 4 0 001.5-5.5 4 4 0 00-5.6-1.5l-5.8 3.3a13.6 13.6 0 00-2.6-2 13.8 13.8 0 00-3-1.3V4.5a4 4 0 00-8.1 0v6.6a14.3 14.3 0 00-5.7 3.2l-5.8-3.3a4 4 0 00-5.6 1.5 4 4 0 001.5 5.5l5.8 3.3a13.7 13.7 0 000 6.5l-5.8 3.2a4 4 0 00-1.5 5.5 4 4 0 003.5 2 4 4 0 002-.5l5.8-3.3a13.6 13.6 0 002.7 2 13.8 13.8 0 003 1.2v6.6a4 4 0 008 0v-6.6a14.2 14.2 0 005.7-3.2l5.8 3.3a4 4 0 002 .5 4 4 0 003.5-2 4 4 0 00-1.3-5.5ZM131 29.7a6 6 0 01-2.3-8.2 6.1 6.1 0 015.3-3 6.2 6.2 0 013 .8 6 6 0 012.2 8.2 6.1 6.1 0 01-8.2 2.2Z" opacity="0"><animateTransform additive="sum" attributeName="transform" dur="6s" repeatCount="indefinite" type="rotate" values="0 136 24; 360 136 24"/><animate id="t3" attributeName="opacity" begin=".83s; t3.end+1s" dur="2s" keyTimes="0; .17; .83; 1" values="0; 1; 1; 0"/></path><animateTransform id="s3" additive="sum" attributeName="transform" begin=".83s; s3.end+1s" dur="2s" type="translate" values="0 -36; 0 92;"/></g></symbol></defs><use xlink:href="#e" width="350" height="222" transform="translate(81 145)"/><use xlink:href="#f" width="156.2" height="49" transform="translate(177.9 337.5)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/thunderstorms-rain.svg b/assets/icons/thunderstorms-rain.svg
new file mode 100644
index 0000000..5d4a654
--- /dev/null
+++ b/assets/icons/thunderstorms-rain.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="8.7" x2="80.9" y1="17.1" y2="142.1" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7b23b"/><stop offset=".5" stop-color="#f7b23b"/><stop offset="1" stop-color="#f59e0b"/></linearGradient><linearGradient id="c" x1="1381.3" x2="1399.5" y1="-1144.7" y2="-1097.4" gradientTransform="rotate(-9 8002.567 8233.063)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0b65ed"/><stop offset=".5" stop-color="#0a5ad4"/><stop offset="1" stop-color="#0950bc"/></linearGradient><linearGradient xlink:href="#c" id="d" x1="1436.7" x2="1454.9" y1="-1137" y2="-1089.7" gradientTransform="rotate(-9 8009.537 8233.037)"/><linearGradient xlink:href="#c" id="e" x1="1492.1" x2="1510.3" y1="-1129.3" y2="-1082.1" gradientTransform="rotate(-9 8016.566 8233.078)"/><symbol id="f" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="g" overflow="visible" viewBox="0 0 129 57"><path fill="url(#c)" stroke="#0a5ad4" stroke-miterlimit="10" d="M8.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x1" additive="sum" attributeName="transform" begin="0s; x1.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y1" attributeName="opacity" begin="0s; y1.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path><path fill="url(#d)" stroke="#0a5ad4" stroke-miterlimit="10" d="M64.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x2" additive="sum" attributeName="transform" begin=".33s; x2.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y2" attributeName="opacity" begin=".33s; y2.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path><path fill="url(#e)" stroke="#0a5ad4" stroke-miterlimit="10" d="M120.5 56.5a8 8 0 01-8-8v-40a8 8 0 0116 0v40a8 8 0 01-8 8Z" opacity="0"><animateTransform id="x3" additive="sum" attributeName="transform" begin="-.33s; x3.end+.33s" dur=".67s" type="translate" values="0 -60; 0 60"/><animate id="y3" attributeName="opacity" begin="-.33s; y3.end+.33s" dur=".67s" keyTimes="0; .25; 1" values="0; 1; 0"/></path></symbol><symbol id="h" viewBox="0 0 102.7 186.8"><path fill="url(#b)" stroke="#f6a823" stroke-miterlimit="10" stroke-width="4" d="m34.8 2-32 96h32l-16 80 80-112h-48l32-64h-48z"><animate id="x1" attributeName="opacity" begin="0s; x1.end+.67s" dur="1.33s" keyTimes="0; .38; .5; .63; .75; .86; .94; 1" values="1; 1; 0; 1; 0; 1; 0; 1"/></path></symbol></defs><use xlink:href="#f" width="350" height="222" transform="translate(81 145)"/><use xlink:href="#g" width="129" height="57" transform="translate(191.5 343.5)"/><use xlink:href="#h" width="102.7" height="186.7" transform="translate(205.23 291)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/thunderstorms.svg b/assets/icons/thunderstorms.svg
new file mode 100644
index 0000000..2f4ea47
--- /dev/null
+++ b/assets/icons/thunderstorms.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="99.5" x2="232.6" y1="30.7" y2="261.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f3f7fe"/><stop offset=".5" stop-color="#f3f7fe"/><stop offset="1" stop-color="#deeafb"/></linearGradient><linearGradient id="b" x1="8.7" x2="80.9" y1="17.1" y2="142.1" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7b23b"/><stop offset=".5" stop-color="#f7b23b"/><stop offset="1" stop-color="#f59e0b"/></linearGradient><symbol id="c" viewBox="0 0 350 222"><path fill="url(#a)" stroke="#e6effc" stroke-miterlimit="10" stroke-width="6" d="m291 107-2.5.1A83.9 83.9 0 00135.6 43 56 56 0 0051 91a56.6 56.6 0 00.8 9A60 60 0 0063 219l4-.2v.2h224a56 56 0 000-112Z"/></symbol><symbol id="d" viewBox="0 0 102.7 186.8"><path fill="url(#b)" stroke="#f6a823" stroke-miterlimit="10" stroke-width="4" d="m34.8 2-32 96h32l-16 80 80-112h-48l32-64h-48z"><animate id="x1" attributeName="opacity" begin="0s; x1.end+.67s" dur="1.33s" keyTimes="0; .38; .5; .63; .75; .86; .94; 1" values="1; 1; 0; 1; 0; 1; 0; 1"/></path></symbol></defs><use xlink:href="#c" width="350" height="222" transform="translate(81 145)"/><use xlink:href="#d" width="102.7" height="186.7" transform="translate(205.23 291)"/></svg>
\ No newline at end of file
diff --git a/assets/icons/wind.svg b/assets/icons/wind.svg
new file mode 100644
index 0000000..56d7854
--- /dev/null
+++ b/assets/icons/wind.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="138.5" x2="224.2" y1="5.1" y2="153.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d4d7dd"/><stop offset=".5" stop-color="#d4d7dd"/><stop offset="1" stop-color="#bec1c6"/></linearGradient><linearGradient xlink:href="#a" id="b" x1="77.7" x2="169" y1="96.2" y2="254.4"/><symbol id="c" viewBox="0 0 348 240"><path fill="none" stroke="url(#a)" stroke-dasharray="148" stroke-linecap="round" stroke-miterlimit="10" stroke-width="24" d="M267.2 24.3A40 40 0 11296 92H12"><animate attributeName="stroke-dashoffset" dur="6s" repeatCount="indefinite" values="0; 2960"/></path><path fill="none" stroke="url(#b)" stroke-dasharray="110" stroke-linecap="round" stroke-miterlimit="10" stroke-width="24" d="M151.2 215.7A40 40 0 10180 148H12"><animate attributeName="stroke-dashoffset" dur="6s" repeatCount="indefinite" values="0; 1540"/></path></symbol></defs><use xlink:href="#c" width="348" height="240" transform="translate(82 136)"/></svg>
\ No newline at end of file
diff --git a/shalom/Cargo.toml b/shalom/Cargo.toml
index 9906bc1..8276f50 100644
--- a/shalom/Cargo.toml
+++ b/shalom/Cargo.toml
@@ -14,6 +14,7 @@ itertools = "0.11"
keyframe = "1.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
strum = { version = "0.25", features = ["derive"] }
tokio = { version = "1.33", features = ["net", "sync", "rt", "macros", "time", "fs"] }
tokio-tungstenite = "0.20"
toml = "0.8"
diff --git a/shalom/src/hass_client.rs b/shalom/src/hass_client.rs
index 84f4e05..8fd2d4a 100644
--- a/shalom/src/hass_client.rs
+++ b/shalom/src/hass_client.rs
@@ -175,11 +175,22 @@ impl HassRequest {
}

pub mod responses {
    use std::borrow::Cow;

    use serde::Deserialize;
    use std::{
        borrow::Cow,
        fmt::{Display, Formatter},
    };

    use serde::{
        de,
        de::{MapAccess, Visitor},
        Deserialize, Deserializer,
    };
    use serde_json::value::RawValue;
    use strum::EnumString;
    use yoke::Yokeable;

    use crate::theme::Icon;

    #[derive(Deserialize, Yokeable, Debug)]
    pub struct AreaRegistryList<'a>(#[serde(borrow)] pub Vec<Area<'a>>);

@@ -268,43 +279,105 @@ pub mod responses {
        pub unique_id: Option<Cow<'a, str>>,
    }

    #[derive(Deserialize, Yokeable, Debug)]
    pub struct StatesList<'a>(#[serde(borrow)] pub Vec<State<'a>>);
    #[derive(Yokeable, Debug, Deserialize)]
    pub struct StatesList<'a>(#[serde(borrow, bound(deserialize = "'a: 'de"))] pub Vec<State<'a>>);

    #[derive(Deserialize, Debug)]
    pub enum State<'a> {
        Sun {
            #[serde(borrow)]
            state: Cow<'a, str>,
            attributes: StateSunAttributes,
        },
        MediaPlayer {
            #[serde(borrow)]
            state: Cow<'a, str>,
            #[serde(borrow)]
            attributes: StateMediaPlayerAttributes<'a>,
        },
        Camera {
            #[serde(borrow)]
            state: Cow<'a, str>,
            #[serde(borrow)]
            attributes: StateCameraAttributes<'a>,
        },
        Weather {
            #[serde(borrow)]
            state: Cow<'a, str>,
            #[serde(borrow)]
            attributes: StateWeatherAttributes<'a>,
        },
        Light {
            #[serde(borrow)]
            state: Cow<'a, str>,
            #[serde(borrow)]
            attributes: StateLightAttributes<'a>,
    #[derive(Debug)]
    pub struct State<'a> {
        pub entity_id: Cow<'a, str>,
        pub state: Cow<'a, str>,
        pub attributes: StateAttributes<'a>,
    }

    impl<'de> Deserialize<'de> for State<'de> {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            deserializer.deserialize_struct(
                "State",
                &["entity_id", "state", "attributes"],
                StateVisitor {},
            )
        }
    }

    pub struct StateVisitor {}

    impl<'de> Visitor<'de> for StateVisitor {
        type Value = State<'de>;

        fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
            formatter.write_str("states struct")
        }

        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
        where
            A: MapAccess<'de>,
        {
            let mut entity_id: Option<Cow<'de, str>> = None;
            let mut state: Option<Cow<'de, str>> = None;
            let mut attributes: Option<&'de RawValue> = None;

            while let Some(key) = map.next_key()? {
                match key {
                    "entity_id" => {
                        entity_id = Some(map.next_value()?);
                    }
                    "state" => {
                        state = Some(map.next_value()?);
                    }
                    "attributes" => {
                        attributes = Some(map.next_value()?);
                    }
                    _ => {
                        let _: &'de RawValue = map.next_value()?;
                    }
                }
            }

            let entity_id = entity_id.ok_or_else(|| de::Error::missing_field("entity_id"))?;
            let state = state.ok_or_else(|| de::Error::missing_field("state"))?;
            let attributes = attributes.ok_or_else(|| de::Error::missing_field("attributes"))?;

            let Some((kind, _)) = entity_id.split_once('.') else {
                return Err(de::Error::custom("invalid entity_id"));
            };

            let attributes = match kind {
                "sun" => StateAttributes::Light(serde_json::from_str(attributes.get()).unwrap()),
                "media_player" => {
                    StateAttributes::MediaPlayer(serde_json::from_str(attributes.get()).unwrap())
                }
                "camera" => {
                    StateAttributes::Camera(serde_json::from_str(attributes.get()).unwrap())
                }
                "weather" => {
                    StateAttributes::Weather(serde_json::from_str(attributes.get()).unwrap())
                }
                "light" => StateAttributes::Light(serde_json::from_str(attributes.get()).unwrap()),
                _ => StateAttributes::Unknown,
            };

            Ok(State {
                entity_id,
                state,
                attributes,
            })
        }
    }

    #[derive(Deserialize, Debug)]
    pub enum StateAttributes<'a> {
        Sun(StateSunAttributes),
        MediaPlayer(#[serde(borrow)] StateMediaPlayerAttributes<'a>),
        Camera(#[serde(borrow)] StateCameraAttributes<'a>),
        Weather(#[serde(borrow)] StateWeatherAttributes<'a>),
        Light(#[serde(borrow)] StateLightAttributes<'a>),
        Unknown,
    }

    #[derive(Deserialize, Debug)]
    pub struct StateSunAttributes {
        // next_dawn: time::OffsetDateTime,
        // next_dusk: time::OffsetDateTime,
@@ -319,27 +392,27 @@ pub mod responses {

    #[derive(Deserialize, Debug)]
    pub struct StateMediaPlayerAttributes<'a> {
        #[serde(borrow)]
        #[serde(borrow, default)]
        source_list: Vec<Cow<'a, str>>,
        #[serde(borrow)]
        #[serde(borrow, default)]
        group_members: Vec<Cow<'a, str>>,
        volume_level: f32,
        is_volume_muted: bool,
        volume_level: Option<f32>,
        is_volume_muted: Option<bool>,
        #[serde(borrow)]
        media_content_id: Cow<'a, str>,
        media_content_id: Option<Cow<'a, str>>,
        #[serde(borrow)]
        media_content_type: Cow<'a, str>,
        media_content_type: Option<Cow<'a, str>>,
        #[serde(borrow)]
        source: Cow<'a, str>,
        shuffle: bool,
        source: Option<Cow<'a, str>>,
        shuffle: Option<bool>,
        #[serde(borrow)]
        repeat: Cow<'a, str>,
        queue_position: u32,
        queue_size: u32,
        repeat: Option<Cow<'a, str>>,
        queue_position: Option<u32>,
        queue_size: Option<u32>,
        #[serde(borrow)]
        device_class: Cow<'a, str>,
        device_class: Option<Cow<'a, str>>,
        #[serde(borrow)]
        friendly_name: Cow<'a, str>,
        friendly_name: Option<Cow<'a, str>>,
    }

    #[derive(Deserialize, Debug)]
@@ -349,72 +422,142 @@ pub mod responses {
        #[serde(borrow)]
        friendly_name: Cow<'a, str>,
        #[serde(borrow)]
        stream_source: Cow<'a, str>,
        stream_source: Option<Cow<'a, str>>,
        #[serde(borrow)]
        still_image_url: Cow<'a, str>,
        still_image_url: Option<Cow<'a, str>>,
        #[serde(borrow)]
        name: Cow<'a, str>,
        name: Option<Cow<'a, str>>,
        #[serde(borrow)]
        id: Cow<'a, str>,
        id: Option<Cow<'a, str>>,
        #[serde(borrow)]
        entity_picture: Cow<'a, str>,
    }

    #[derive(Deserialize, Debug, EnumString, Copy, Clone)]
    #[serde(rename_all = "kebab-case")]
    #[strum(serialize_all = "kebab-case")]
    pub enum WeatherCondition {
        ClearNight,
        Cloudy,
        Fog,
        Hail,
        Lightning,
        LightningRainy,
        #[serde(rename = "partlycloudy")]
        #[strum(serialize = "partlycloudy")]
        PartlyCloudy,
        Pouring,
        Rainy,
        Snowy,
        SnowyRainy,
        Sunny,
        Windy,
        WindyVariant,
        Exceptional,
        #[serde(other)]
        Unknown,
    }

    impl WeatherCondition {
        pub fn icon(self, day_time: bool) -> Option<Icon> {
            match self {
                WeatherCondition::ClearNight => Some(Icon::ClearNight),
                WeatherCondition::Cloudy => Some(Icon::Cloud),
                WeatherCondition::Fog => Some(Icon::Fog),
                WeatherCondition::Hail => Some(Icon::Hail),
                WeatherCondition::Lightning => Some(Icon::Thunderstorms),
                WeatherCondition::LightningRainy => Some(Icon::ThunderstormsRain),
                WeatherCondition::PartlyCloudy => Some(if day_time {
                    Icon::PartlyCloudyDay
                } else {
                    Icon::PartlyCloudyNight
                }),
                WeatherCondition::Pouring => Some(Icon::ExtremeRain),
                WeatherCondition::Rainy => Some(Icon::Rain),
                WeatherCondition::Snowy | WeatherCondition::SnowyRainy => Some(Icon::Snow),
                WeatherCondition::Sunny => Some(Icon::ClearDay),
                WeatherCondition::Windy | WeatherCondition::WindyVariant => Some(Icon::Wind),
                WeatherCondition::Exceptional | WeatherCondition::Unknown => None,
            }
        }
    }

    impl Display for WeatherCondition {
        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
            f.write_str(match self {
                WeatherCondition::ClearNight => "Clear",
                WeatherCondition::Cloudy => "Cloudy",
                WeatherCondition::Fog => "Fog",
                WeatherCondition::Hail => "Hail",
                WeatherCondition::Lightning | WeatherCondition::LightningRainy => "Lightning",
                WeatherCondition::PartlyCloudy => "Partly Cloudy",
                WeatherCondition::Pouring => "Heavy Rain",
                WeatherCondition::Rainy => "Rain",
                WeatherCondition::Snowy | WeatherCondition::SnowyRainy => "Snow",
                WeatherCondition::Sunny => "Sunny",
                WeatherCondition::Windy | WeatherCondition::WindyVariant => "Windy",
                WeatherCondition::Exceptional => "Exceptional",
                WeatherCondition::Unknown => "Unknown",
            })
        }
    }

    #[derive(Deserialize, Debug)]
    pub struct StateWeatherAttributes<'a> {
        temperature: f32,
        dew_point: f32,
        pub temperature: f32,
        pub dew_point: f32,
        #[serde(borrow)]
        temperature_unit: Cow<'a, str>,
        humidity: u8,
        cloud_coverage: u8,
        pressure: f32,
        pub temperature_unit: Cow<'a, str>,
        pub humidity: f32,
        pub cloud_coverage: f32,
        pub pressure: f32,
        #[serde(borrow)]
        pressure_unit: Cow<'a, str>,
        wind_bearing: f32,
        wind_speed: f32,
        pub pressure_unit: Cow<'a, str>,
        pub wind_bearing: f32,
        pub wind_speed: f32,
        #[serde(borrow)]
        wind_speed_unit: Cow<'a, str>,
        pub wind_speed_unit: Cow<'a, str>,
        #[serde(borrow)]
        visibility_unit: Cow<'a, str>,
        pub visibility_unit: Cow<'a, str>,
        #[serde(borrow)]
        precipitation_unit: Cow<'a, str>,
        pub precipitation_unit: Cow<'a, str>,
        #[serde(borrow)]
        forecast: Vec<StateWeatherAttributesForecast<'a>>,
        pub forecast: Vec<StateWeatherAttributesForecast<'a>>,
    }

    #[derive(Deserialize, Debug)]
    pub struct StateWeatherAttributesForecast<'a> {
        #[serde(borrow)]
        condition: Cow<'a, str>,
        pub condition: Cow<'a, str>,
        // datetime: time::OffsetDateTime,
        wind_bearing: f32,
        temperature: f32,
        pub wind_bearing: f32,
        pub temperature: f32,
        #[serde(rename = "templow")]
        temperature_low: f32,
        wind_speed: f32,
        precipitation: u8,
        humidity: u8,
        pub temperature_low: f32,
        pub wind_speed: f32,
        pub precipitation: f32,
        pub humidity: f32,
    }

    #[derive(Deserialize, Debug)]
    pub struct StateLightAttributes<'a> {
        min_color_temp_kelvin: u16,
        max_color_temp_kelvin: u16,
        min_mireds: u16,
        max_mireds: u16,
        min_color_temp_kelvin: Option<u16>,
        max_color_temp_kelvin: Option<u16>,
        min_mireds: Option<u16>,
        max_mireds: Option<u16>,
        #[serde(default)]
        supported_color_modes: Vec<ColorMode>,
        #[serde(borrow)]
        mode: Cow<'a, str>,
        mode: Option<Cow<'a, str>>,
        #[serde(borrow)]
        dynamics: Cow<'a, str>,
        dynamics: Option<Cow<'a, str>>,
        #[serde(borrow)]
        friendly_name: Cow<'a, str>,
        color_mode: Option<ColorMode>,
        brightness: Option<u8>,
        brightness: Option<f32>,
        color_temp_kelvin: Option<u16>,
        color_temp: Option<u16>,
        xy_color: Option<(u8, u8)>,
        xy_color: Option<(f32, f32)>,
    }

    #[derive(Deserialize, Debug)]
diff --git a/shalom/src/oracle.rs b/shalom/src/oracle.rs
index 3a0c878..d1acf95 100644
--- a/shalom/src/oracle.rs
+++ b/shalom/src/oracle.rs
@@ -1,21 +1,28 @@
use std::collections::BTreeMap;
use std::{collections::BTreeMap, str::FromStr};

use internment::Intern;

use crate::hass_client::{responses::AreaRegistryList, HassRequestKind};
use crate::hass_client::{
    responses::{AreaRegistryList, StateAttributes, StatesList, WeatherCondition},
    HassRequestKind,
};

#[derive(Debug)]
pub struct Oracle {
    client: crate::hass_client::Client,
    rooms: BTreeMap<Intern<str>, Room>,
    pub weather: Weather,
}

impl Oracle {
    pub async fn new(hass_client: crate::hass_client::Client) -> Self {
        let (rooms,) = tokio::join!(
            hass_client.request::<AreaRegistryList<'_>>(HassRequestKind::AreaRegistry)
        let (rooms, states) = tokio::join!(
            hass_client.request::<AreaRegistryList<'_>>(HassRequestKind::AreaRegistry),
            hass_client.request::<StatesList<'_>>(HassRequestKind::GetStates),
        );

        let states = states.get();

        let rooms = rooms
            .get()
            .0
@@ -33,6 +40,7 @@ impl Oracle {
        Self {
            client: hass_client,
            rooms,
            weather: Weather::parse_from_states(states),
        }
    }

@@ -45,3 +53,44 @@ impl Oracle {
pub struct Room {
    pub name: Intern<str>,
}

#[derive(Debug)]
pub struct Weather {
    pub temperature: i16,
    pub high: i16,
    pub low: i16,
    pub condition: WeatherCondition,
}

impl Weather {
    fn parse_from_states(states: &StatesList) -> Self {
        let (state, weather) = states
            .0
            .iter()
            .filter_map(|v| match &v.attributes {
                StateAttributes::Weather(attr) => Some((&v.state, attr)),
                _ => None,
            })
            .next()
            .unwrap();

        let condition = WeatherCondition::from_str(&state).unwrap_or(WeatherCondition::Unknown);

        let (high, low) =
            weather
                .forecast
                .iter()
                .fold((i16::MIN, i16::MAX), |(high, low), curr| {
                    let temp = curr.temperature.round() as i16;

                    (high.max(temp), low.min(temp))
                });

        Self {
            temperature: weather.temperature.round() as i16,
            condition,
            high,
            low,
        }
    }
}
diff --git a/shalom/src/pages/omni.rs b/shalom/src/pages/omni.rs
index 0ea17be..ed9b8fc 100644
--- a/shalom/src/pages/omni.rs
+++ b/shalom/src/pages/omni.rs
@@ -32,13 +32,11 @@ impl<M: Clone> Component<M, Renderer> for Omni<M> {
    }

    fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> {
        let header = |v| {
            text(v).size(60).font(Font {
                weight: Weight::Bold,
                stretch: Stretch::Condensed,
                ..Font::with_name("Helvetica Neue")
            })
        };
        let greeting = text("Good Evening").size(60).font(Font {
            weight: Weight::Bold,
            stretch: Stretch::Condensed,
            ..Font::with_name("Helvetica Neue")
        });

        let room = |room, image| {
            image_card::image_card(image, room).on_press(Event::OpenRoom(room))
@@ -56,9 +54,13 @@ impl<M: Clone> Component<M, Renderer> for Omni<M> {
            .fold(Column::new().spacing(10), Column::push);

        scrollable(
            column![header("Cameras"), header("Rooms"), rooms,]
                .spacing(20)
                .padding(40),
            column![
                greeting,
                crate::widgets::cards::weather::WeatherCard::new(self.oracle.clone()),
                rooms,
            ]
            .spacing(20)
            .padding(40),
        )
        .into()
    }
diff --git a/shalom/src/theme.rs b/shalom/src/theme.rs
index 3f57e9c..77466fc 100644
--- a/shalom/src/theme.rs
+++ b/shalom/src/theme.rs
@@ -43,30 +43,56 @@ pub enum Icon {
    Play,
    Pause,
    Repeat,
    Cloud,
    ClearNight,
    Fog,
    Hail,
    Thunderstorms,
    ThunderstormsRain,
    PartlyCloudyDay,
    PartlyCloudyNight,
    ExtremeRain,
    Rain,
    Snow,
    ClearDay,
    Wind,
}

impl Icon {
    pub fn handle(self) -> svg::Handle {
        macro_rules! image {
            ($path:expr) => {{
                static FILE: &[u8] = include_bytes!($path);
                static FILE: &[u8] = include_bytes!(concat!("../../assets/icons/", $path, ".svg"));
                static HANDLE: Lazy<svg::Handle> = Lazy::new(|| svg::Handle::from_memory(FILE));
                (*HANDLE).clone()
            }};
        }

        match self {
            Self::Home => image!("../../assets/icons/home.svg"),
            Self::Back => image!("../../assets/icons/back.svg"),
            Self::Bulb => image!("../../assets/icons/light-bulb.svg"),
            Self::Hamburger => image!("../../assets/icons/hamburger.svg"),
            Self::Speaker => image!("../../assets/icons/speaker.svg"),
            Self::SpeakerMuted => image!("../../assets/icons/speaker-muted.svg"),
            Self::Backward => image!("../../assets/icons/backward.svg"),
            Self::Forward => image!("../../assets/icons/forward.svg"),
            Self::Play => image!("../../assets/icons/play.svg"),
            Self::Pause => image!("../../assets/icons/pause.svg"),
            Self::Repeat => image!("../../assets/icons/repeat.svg"),
            Self::Home => image!("home"),
            Self::Back => image!("back"),
            Self::Bulb => image!("light-bulb"),
            Self::Hamburger => image!("hamburger"),
            Self::Speaker => image!("speaker"),
            Self::SpeakerMuted => image!("speaker-muted"),
            Self::Backward => image!("backward"),
            Self::Forward => image!("forward"),
            Self::Play => image!("play"),
            Self::Pause => image!("pause"),
            Self::Repeat => image!("repeat"),
            Self::Cloud => image!("cloud"),
            Self::ClearNight => image!("clear-night"),
            Self::Fog => image!("fog"),
            Self::Hail => image!("hail"),
            Self::Thunderstorms => image!("thunderstorms"),
            Self::ThunderstormsRain => image!("thunderstorms-rain"),
            Self::PartlyCloudyDay => image!("partly-cloudy-day"),
            Self::PartlyCloudyNight => image!("partly-cloudy-night"),
            Self::ExtremeRain => image!("extreme-rain"),
            Self::Rain => image!("rain"),
            Self::Snow => image!("snow"),
            Self::ClearDay => image!("clear-day"),
            Self::Wind => image!("wind"),
        }
    }
}
diff --git a/shalom/src/widgets/cards/mod.rs b/shalom/src/widgets/cards/mod.rs
new file mode 100644
index 0000000..ef98bcf
--- /dev/null
+++ b/shalom/src/widgets/cards/mod.rs
@@ -0,0 +1 @@
pub mod weather;
diff --git a/shalom/src/widgets/cards/weather.rs b/shalom/src/widgets/cards/weather.rs
new file mode 100644
index 0000000..473f4eb
--- /dev/null
+++ b/shalom/src/widgets/cards/weather.rs
@@ -0,0 +1,173 @@
use std::sync::Arc;

use iced::{
    advanced::{
        layout::{Limits, Node},
        renderer::{Quad, Style},
        svg::Renderer as SvgRenderer,
        text::{LineHeight, Renderer as TextRenderer, Shaping},
        widget::Tree,
        Layout, Renderer as AdvancedRenderer, Text, Widget,
    },
    alignment::{Horizontal, Vertical},
    font::Weight,
    gradient::Linear,
    mouse::Cursor,
    Alignment, Background, Color, Degrees, Element, Font, Gradient, Length, Rectangle, Renderer,
    Size, Theme,
};

use crate::oracle::Oracle;

pub struct WeatherCard<M> {
    pub on_click: Option<M>,
    pub oracle: Arc<Oracle>,
}

impl<M> WeatherCard<M> {
    pub fn new(oracle: Arc<Oracle>) -> Self {
        Self {
            on_click: None,
            oracle,
        }
    }

    fn build_temperature(&self) -> String {
        format!("{}°", self.oracle.weather.temperature)
    }

    fn build_conditions(&self) -> String {
        format!(
            "{}\nH:{}° L:{}°",
            self.oracle.weather.condition, self.oracle.weather.high, self.oracle.weather.low,
        )
    }
}

impl<M: Clone> Widget<M, Renderer> for WeatherCard<M> {
    fn width(&self) -> Length {
        Length::Fixed(192.)
    }

    fn height(&self) -> Length {
        Length::Fixed(192.)
    }

    fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node {
        let padding = 16.into();

        let limits = limits
            .height(self.height())
            .width(self.width())
            .pad(padding);
        let container_size = limits.resolve(Size::ZERO);

        let mut header_node = Node::new(renderer.measure(
            &self.build_temperature(),
            42.,
            LineHeight::default(),
            Font {
                weight: Weight::Normal,
                ..Font::with_name("Helvetica Neue")
            },
            container_size,
            Shaping::Basic,
        ));
        header_node.move_to([padding.top, padding.left].into());
        header_node.align(Alignment::Start, Alignment::Start, container_size);

        let mut icon_node =
            Node::new(Size::new(16., 16.)).translate([padding.left, -padding.bottom - 32.].into());
        icon_node.align(Alignment::Start, Alignment::End, container_size);

        let mut conditions_node = Node::new(renderer.measure(
            &self.build_conditions(),
            12.,
            LineHeight::default(),
            Font {
                weight: Weight::Bold,
                ..Font::with_name("Helvetica Neue")
            },
            container_size,
            Shaping::Basic,
        ))
        .translate([padding.left, -padding.bottom].into());
        conditions_node.align(Alignment::Start, Alignment::End, container_size);

        Node::with_children(
            container_size,
            vec![header_node, icon_node, conditions_node],
        )
    }

    fn draw(
        &self,
        _state: &Tree,
        renderer: &mut Renderer,
        _theme: &Theme,
        _style: &Style,
        layout: Layout<'_>,
        _cursor: Cursor,
        _viewport: &Rectangle,
    ) {
        renderer.fill_quad(
            Quad {
                bounds: layout.bounds(),
                border_radius: [20., 20., 20., 20.].into(),
                border_width: 0.,
                border_color: Color::WHITE,
            },
            Background::Gradient(Gradient::Linear(
                Linear::new(Degrees(90.))
                    .add_stop(0.0, Color::from_rgba8(43, 44, 66, 1.0))
                    .add_stop(1.0, Color::from_rgba8(15, 18, 27, 1.0)),
            )),
        );

        let mut children = layout.children();

        renderer.fill_text(Text {
            content: &self.build_temperature(),
            bounds: children.next().unwrap().bounds(),
            size: 42.,
            line_height: LineHeight::default(),
            color: Color::WHITE,
            font: Font {
                weight: Weight::Normal,
                ..Font::with_name("Helvetica Neue")
            },
            horizontal_alignment: Horizontal::Left,
            vertical_alignment: Vertical::Top,
            shaping: Shaping::Basic,
        });

        let icon_bounds = children.next().unwrap().bounds();
        if let Some(icon) = self.oracle.weather.condition.icon(false) {
            renderer.draw(icon.handle(), None, icon_bounds);
        }

        renderer.fill_text(Text {
            content: &self.build_conditions(),
            bounds: children.next().unwrap().bounds(),
            size: 12.,
            line_height: LineHeight::default(),
            color: Color::WHITE,
            font: Font {
                weight: Weight::Bold,
                ..Font::with_name("Helvetica Neue")
            },
            horizontal_alignment: Horizontal::Left,
            vertical_alignment: Vertical::Top,
            shaping: Shaping::Basic,
        });
    }
}

impl<'a, M> From<WeatherCard<M>> for Element<'a, M>
where
    M: 'a + Clone,
{
    fn from(modal: WeatherCard<M>) -> Self {
        Element::new(modal)
    }
}
diff --git a/shalom/src/widgets/mod.rs b/shalom/src/widgets/mod.rs
index 413cbd5..e2450e3 100644
--- a/shalom/src/widgets/mod.rs
+++ b/shalom/src/widgets/mod.rs
@@ -1,3 +1,4 @@
pub mod cards;
pub mod context_menu;
pub mod image_card;
pub mod media_player;