diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx
index 4397553..9682d89 100644
--- a/apps/dashboard/src/App.tsx
+++ b/apps/dashboard/src/App.tsx
@@ -1,12 +1,20 @@
import { type JrpcRequest, type JrpcResponse, newJrpcRequestId } from "@eva/jrpc"
import { ZIGBEE_DEVICE, type ZigbeeDeviceName } from "@eva/zigbee"
import { useQuery } from "@tanstack/react-query"
-import { useDrag } from "@use-gesture/react"
import Chart from "chart.js/auto"
-import { atom, useAtomValue, useSetAtom, useStore } from "jotai"
+import { useStore } from "jotai"
import { useEffect, useLayoutEffect, useRef, useState } from "react"
import { beszelSystemsQuery } from "./beszel"
import cn from "./components/lib/cn"
+import { Tile } from "./components/tile"
+import {
+ LightControlTile,
+ type LightSceneConfig,
+ LightSceneTile,
+ brightnessStepAtoms,
+ brightnessToStep,
+ stepToBrightness,
+} from "./light-control"
import { StatusSeverity, TubeLine, formatLineName, tflDisruptionsQuery } from "./tfl"
import {
DEFAULT_LATITUDE,
@@ -17,41 +25,6 @@ import {
weatherDescriptionQuery,
} from "./weather"
-const LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT = 44
-
-// Store brightness as step (0-43) to match the 44 bars exactly
-// Step 0 = OFF, Steps 1-43 map to bars 42-0
-const brightnessStepAtoms = atom({
- [ZIGBEE_DEVICE.deskLamp]: atom(0),
- [ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(0),
-})
-
-const intermediateBrightnessStepAtoms = atom({
- [ZIGBEE_DEVICE.deskLamp]: atom(-1),
- [ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(-1),
-})
-
-// Convert brightness (0-254) to step (0-43)
-// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
-function brightnessToStep(brightness: number): number {
- if (brightness === 0) return 0
- // Map brightness 1-254 to steps 1-43
- return Math.max(1, Math.round((brightness / 254) * (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)))
-}
-
-// Convert step (0-43) to brightness (0-254)
-// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
-function stepToBrightness(step: number): number {
- if (step === 0) return 0
- // Map steps 1-43 to brightness 1-254
- return Math.max(1, Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 254))
-}
-
-const DEVICE_FRIENDLY_NAMES = {
- [ZIGBEE_DEVICE.deskLamp]: "Desk Lamp",
- [ZIGBEE_DEVICE.livingRoomFloorLamp]: "Floor Lamp",
-} as const
-
function App() {
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"
const wsHost = import.meta.env.VITE_API_HOST || window.location.host
@@ -117,6 +90,22 @@ function App() {
ws.send(JSON.stringify(req))
}
+ function setScene(scene: LightSceneConfig) {
+ const ws = websocket.current
+ for (const [deviceName, state] of Object.entries(scene.deviceStates)) {
+ const req: JrpcRequest<"setDeviceState"> = {
+ id: newJrpcRequestId(),
+ jsonrpc: "2.0",
+ method: "setDeviceState",
+ params: {
+ deviceName: deviceName as ZigbeeDeviceName,
+ state,
+ },
+ }
+ ws.send(JSON.stringify(req))
+ }
+ }
+
return (
@@ -128,40 +117,34 @@ function App() {
+ {
+ setScene(scene)
+ }}
+ />
+
{
setBrightnessStep(ZIGBEE_DEVICE.livingRoomFloorLamp, step)
}}
/>
{
setBrightnessStep(ZIGBEE_DEVICE.deskLamp, step)
}}
/>
-
+
)
}
-function Tile({ children, className }: { children?: React.ReactNode; className?: string }) {
- return (
-
- {children}
-
- )
-}
-
function DateTimeTile() {
const [time, setTime] = useState(new Date())
@@ -641,228 +624,4 @@ function SystemTile({
)
}
-function LightControlTile({
- deviceName,
- className,
- onRequestBrightnessStepChange,
-}: { deviceName: ZigbeeDeviceName; className?: string; onRequestBrightnessStepChange: (step: number) => void }) {
- const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
- // Map step to bar index for thumb position
- // Step 0 = OFF (no thumb shown, set to invalid index)
- // Step 1-43 map to bars 42-0
- const initialHighlightIndexStart =
- currentBrightnessStep === 0
- ? LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 // No thumb (index out of range, but no bars highlighted)
- : LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - currentBrightnessStep
- const touchContainerRef = useRef(null)
- const barRefs = useRef<(HTMLDivElement | null)[]>(
- Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }, () => null),
- )
- const setIntermediateBrightnessStep = useSetAtom(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
- const store = useStore()
-
- useEffect(() => {
- const brightnessStepAtom = store.get(brightnessStepAtoms)[deviceName]
- if (store.get(brightnessStepAtom) === currentBrightnessStep) {
- setIntermediateBrightnessStep(-1)
- }
- }, [currentBrightnessStep, deviceName, setIntermediateBrightnessStep, store])
-
- const bind = useDrag(({ xy: [x], first, last }) => {
- if (!touchContainerRef.current) return
-
- if (!first) {
- touchContainerRef.current.dataset.active = "true"
- }
-
- if (last) {
- delete touchContainerRef.current.dataset.active
- let thumbIndex = -1
- for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
- const bar = barRefs.current[i]
- if (!bar) continue
-
- const barRect = bar.getBoundingClientRect()
-
- if (x >= barRect.left - 2 && x < barRect.right + 2 && thumbIndex === -1) {
- thumbIndex = i
- bar.dataset.thumb = "true"
- } else {
- delete bar.dataset.thumb
- }
-
- delete bar.dataset.touched
- delete bar.dataset.touchProximity
- }
-
- if (thumbIndex !== -1) {
- // Map bar index to step: bar 42 -> step 1, bar 0 -> step 43
- const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - thumbIndex
- onRequestBrightnessStepChange(step)
- } else {
- const firstElement = barRefs.current[barRefs.current.length - 1]
- const lastElement = barRefs.current[0]
- if (lastElement && x > lastElement.getBoundingClientRect().right) {
- lastElement.dataset.thumb = "true"
- setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
- if (last) {
- onRequestBrightnessStepChange(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
- }
- } else if (firstElement && x < firstElement.getBoundingClientRect().left) {
- firstElement.dataset.thumb = "true"
- setIntermediateBrightnessStep(0)
- if (last) {
- onRequestBrightnessStepChange(0)
- }
- }
- }
- } else {
- let touchedIndex = -1
- for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
- const bar = barRefs.current[i]
- if (!bar) continue
-
- const barRect = bar.getBoundingClientRect()
-
- delete bar.dataset.thumb
-
- if (x >= barRect.left - 2 && x < barRect.right + 2 && touchedIndex === -1) {
- touchedIndex = i
-
- bar.dataset.touched = "true"
- bar.dataset.highlighted = "false"
- delete bar.dataset.touchProximity
-
- const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - i - 1
- setIntermediateBrightnessStep(step)
-
- if (barRefs.current[i - 1]) {
- barRefs.current[i - 1]!.dataset.touchProximity = "close"
- }
- if (barRefs.current[i - 2]) {
- barRefs.current[i - 2]!.dataset.touchProximity = "medium"
- }
- if (barRefs.current[i - 3]) {
- barRefs.current[i - 3]!.dataset.touchProximity = "far"
- }
- } else if (barRect.left < x) {
- if (bar.dataset.touched === "true") {
- bar.dataset.prevTouched = "true"
- } else {
- delete bar.dataset.prevTouched
- }
- bar.dataset.touched = "false"
- bar.dataset.highlighted = "true"
- if (touchedIndex >= 0) {
- const diff = i - touchedIndex
- if (diff === 1) {
- bar.dataset.touchProximity = "close"
- } else if (diff === 2) {
- bar.dataset.touchProximity = "medium"
- } else if (diff === 3) {
- bar.dataset.touchProximity = "far"
- } else {
- delete bar.dataset.touchProximity
- }
- } else {
- delete bar.dataset.touchProximity
- }
- } else if (barRect.right > x) {
- bar.dataset.highlighted = "false"
- bar.dataset.touched = "false"
- if (touchedIndex >= 0) {
- const diff = i - touchedIndex
- if (diff === 1) {
- bar.dataset.touchProximity = "close"
- } else if (diff === 2) {
- bar.dataset.touchProximity = "medium"
- } else if (diff === 3) {
- bar.dataset.touchProximity = "far"
- } else {
- delete bar.dataset.touchProximity
- }
- } else {
- delete bar.dataset.touchProximity
- }
- } else {
- bar.dataset.touched = "false"
- bar.dataset.highlighted = "false"
- delete bar.dataset.touchProximity
- }
- }
-
- if (touchedIndex === -1) {
- const firstElement = barRefs.current[barRefs.current.length - 1]
- const lastElement = barRefs.current[0]
- if (lastElement && x > lastElement.getBoundingClientRect().right) {
- lastElement.dataset.thumb = "true"
- setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
- } else if (firstElement && x < firstElement.getBoundingClientRect().left) {
- firstElement.dataset.thumb = "true"
- setIntermediateBrightnessStep(0)
- }
- }
- }
- })
-
- return (
-
-
- {Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }).map((_, index) => {
- const highlighted = index > initialHighlightIndexStart
- return (
-
{
- barRefs.current[index] = ref
- }}
- // biome-ignore lint/suspicious/noArrayIndexKey:
- key={index}
- className="transition-all transition-75 w-[2px] h-[2px] bg-neutral-400 rounded-full data-[highlighted=true]:h-2 data-[touch-proximity=close]:h-6 data-[touch-proximity=medium]:h-4 data-[touch-proximity=far]:h-2 data-[highlighted=true]:bg-teal-500 data-[touched=true]:h-8 data-[touched=true]:w-1 data-[touched=true]:bg-teal-500 data-[touched=true]:transition-none data-[prev-touched=true]:transition-none data-[thumb=true]:h-8 data-[thumb=true]:bg-teal-500"
- />
- )
- })}
-
-
-
{DEVICE_FRIENDLY_NAMES[deviceName]}
-
-
-
- )
-}
-
-function BrightnessLevelLabel({ deviceName }: { deviceName: ZigbeeDeviceName }) {
- const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
- const intermediateBrightnessStep = useAtomValue(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
-
- const step = intermediateBrightnessStep === -1 ? currentBrightnessStep : intermediateBrightnessStep
-
- let label: string
- if (step === 0) {
- label = "OFF"
- } else {
- // Convert step to percentage: step 1 = ~2%, step 43 = 100%
- const brightnessPercentage = Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 100)
- label = `${brightnessPercentage}%`
- }
-
- return (
-
- {label}
-
- )
-}
-
export default App
diff --git a/apps/dashboard/src/components/tile.tsx b/apps/dashboard/src/components/tile.tsx
new file mode 100644
index 0000000..b3e1af9
--- /dev/null
+++ b/apps/dashboard/src/components/tile.tsx
@@ -0,0 +1,14 @@
+import cn from "./lib/cn"
+
+export function Tile({ children, className }: { children?: React.ReactNode; className?: string }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/dashboard/src/light-control.tsx b/apps/dashboard/src/light-control.tsx
new file mode 100644
index 0000000..ac138d4
--- /dev/null
+++ b/apps/dashboard/src/light-control.tsx
@@ -0,0 +1,359 @@
+import { ZIGBEE_DEVICE, type ZigbeeDeviceName, ZigbeeDeviceState, type ZigbeeDeviceStates } from "@eva/zigbee"
+import { useDrag } from "@use-gesture/react"
+import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
+import { CloudyIcon, LightbulbOffIcon, type LucideIcon, MoonStarIcon } from "lucide-react"
+import { useEffect, useRef } from "react"
+import cn from "./components/lib/cn"
+import { Tile } from "./components/tile"
+
+const LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT = 40
+
+// Store brightness as step (0-43) to match the 44 bars exactly
+// Step 0 = OFF, Steps 1-43 map to bars 42-0
+export const brightnessStepAtoms = atom({
+ [ZIGBEE_DEVICE.deskLamp]: atom(0),
+ [ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(0),
+})
+
+export const intermediateBrightnessStepAtoms = atom({
+ [ZIGBEE_DEVICE.deskLamp]: atom(-1),
+ [ZIGBEE_DEVICE.livingRoomFloorLamp]: atom(-1),
+})
+
+const sceneAtom = atom
(null)
+
+const DEVICE_FRIENDLY_NAMES = {
+ [ZIGBEE_DEVICE.deskLamp]: "Desk Lamp",
+ [ZIGBEE_DEVICE.livingRoomFloorLamp]: "Floor Lamp",
+} as const
+
+export type LightSceneConfig = {
+ id: string
+ name: string
+ icon: LucideIcon
+ deviceStates: Partial
+}
+
+const DEFAULT_SCENES: Record = {
+ "lights-off": {
+ id: "lights-off",
+ name: "Lights off",
+ icon: LightbulbOffIcon,
+ deviceStates: {
+ [ZIGBEE_DEVICE.deskLamp]: {
+ state: "OFF",
+ brightness: 0,
+ },
+ [ZIGBEE_DEVICE.livingRoomFloorLamp]: {
+ state: "OFF",
+ brightness: 0,
+ },
+ },
+ },
+ evening: {
+ id: "evening",
+ name: "Evening",
+ icon: MoonStarIcon,
+ deviceStates: {
+ [ZIGBEE_DEVICE.deskLamp]: {
+ state: "ON",
+ brightness: 127,
+ },
+ [ZIGBEE_DEVICE.livingRoomFloorLamp]: {
+ state: "ON",
+ brightness: 254,
+ },
+ },
+ },
+ gloomy: {
+ id: "gloomy",
+ name: "Gloomy",
+ icon: CloudyIcon,
+ deviceStates: {
+ [ZIGBEE_DEVICE.deskLamp]: {
+ state: "ON",
+ brightness: 50,
+ },
+ [ZIGBEE_DEVICE.livingRoomFloorLamp]: {
+ state: "ON",
+ brightness: 128,
+ },
+ },
+ },
+} as const
+
+// Convert brightness (0-254) to step (0-43)
+// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
+export function brightnessToStep(brightness: number): number {
+ if (brightness === 0) return 0
+ // Map brightness 1-254 to steps 1-43
+ return Math.max(1, Math.round((brightness / 254) * (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)))
+}
+
+// Convert step (0-43) to brightness (0-254)
+// Step 0 = brightness 0, steps 1-43 map to brightness 1-254
+export function stepToBrightness(step: number): number {
+ if (step === 0) return 0
+ // Map steps 1-43 to brightness 1-254
+ return Math.max(1, Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 254))
+}
+
+export function LightControlTile({
+ deviceName,
+ className,
+ onRequestBrightnessStepChange,
+}: { deviceName: ZigbeeDeviceName; className?: string; onRequestBrightnessStepChange: (step: number) => void }) {
+ const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
+ // Map step to bar index for thumb position
+ // Step 0 = OFF (no thumb shown, set to invalid index)
+ // Step 1-43 map to bars 42-0
+ const initialHighlightIndexStart =
+ currentBrightnessStep === 0
+ ? LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 // No thumb (index out of range, but no bars highlighted)
+ : LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - currentBrightnessStep
+ const touchContainerRef = useRef(null)
+ const barRefs = useRef<(HTMLDivElement | null)[]>(
+ Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }, () => null),
+ )
+ const setIntermediateBrightnessStep = useSetAtom(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
+ const setScene = useSetAtom(sceneAtom)
+ const store = useStore()
+
+ useEffect(() => {
+ const brightnessStepAtom = store.get(brightnessStepAtoms)[deviceName]
+ if (store.get(brightnessStepAtom) === currentBrightnessStep) {
+ setIntermediateBrightnessStep(-1)
+ }
+ }, [currentBrightnessStep, deviceName, setIntermediateBrightnessStep, store])
+
+ function requestBrightnessStepChange(step: number) {
+ onRequestBrightnessStepChange(step)
+ setScene(null)
+ }
+
+ const bind = useDrag(({ xy: [x], first, last }) => {
+ if (!touchContainerRef.current) return
+
+ if (!first) {
+ touchContainerRef.current.dataset.active = "true"
+ }
+
+ if (last) {
+ delete touchContainerRef.current.dataset.active
+ let thumbIndex = -1
+ for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
+ const bar = barRefs.current[i]
+ if (!bar) continue
+
+ const barRect = bar.getBoundingClientRect()
+
+ if (x >= barRect.left - 2 && x < barRect.right + 2 && thumbIndex === -1) {
+ thumbIndex = i
+ bar.dataset.thumb = "true"
+ } else {
+ delete bar.dataset.thumb
+ }
+
+ delete bar.dataset.touched
+ delete bar.dataset.touchProximity
+ }
+
+ if (thumbIndex !== -1) {
+ // Map bar index to step: bar 42 -> step 1, bar 0 -> step 43
+ const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1 - thumbIndex
+ requestBrightnessStepChange(step)
+ } else {
+ const firstElement = barRefs.current[barRefs.current.length - 1]
+ const lastElement = barRefs.current[0]
+ if (lastElement && x > lastElement.getBoundingClientRect().right) {
+ lastElement.dataset.thumb = "true"
+ setIntermediateBrightnessStep(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
+ if (last) {
+ requestBrightnessStepChange(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
+ }
+ } else if (firstElement && x < firstElement.getBoundingClientRect().left) {
+ firstElement.dataset.thumb = "true"
+ setIntermediateBrightnessStep(0)
+ if (last) {
+ requestBrightnessStepChange(0)
+ }
+ }
+ }
+ } else {
+ let touchedIndex = -1
+ for (let i = 0; i < LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT; i++) {
+ const bar = barRefs.current[i]
+ if (!bar) continue
+
+ const barRect = bar.getBoundingClientRect()
+
+ delete bar.dataset.thumb
+
+ if (x >= barRect.left - 2 && x < barRect.right + 2 && touchedIndex === -1) {
+ touchedIndex = i
+
+ bar.dataset.touched = "true"
+ bar.dataset.highlighted = "false"
+ delete bar.dataset.touchProximity
+
+ const step = LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - i - 1
+ requestBrightnessStepChange(step)
+
+ if (barRefs.current[i - 1]) {
+ barRefs.current[i - 1]!.dataset.touchProximity = "close"
+ }
+ if (barRefs.current[i - 2]) {
+ barRefs.current[i - 2]!.dataset.touchProximity = "medium"
+ }
+ if (barRefs.current[i - 3]) {
+ barRefs.current[i - 3]!.dataset.touchProximity = "far"
+ }
+ } else if (barRect.left < x) {
+ if (bar.dataset.touched === "true") {
+ bar.dataset.prevTouched = "true"
+ } else {
+ delete bar.dataset.prevTouched
+ }
+ bar.dataset.touched = "false"
+ bar.dataset.highlighted = "true"
+ if (touchedIndex >= 0) {
+ const diff = i - touchedIndex
+ if (diff === 1) {
+ bar.dataset.touchProximity = "close"
+ } else if (diff === 2) {
+ bar.dataset.touchProximity = "medium"
+ } else if (diff === 3) {
+ bar.dataset.touchProximity = "far"
+ } else {
+ delete bar.dataset.touchProximity
+ }
+ } else {
+ delete bar.dataset.touchProximity
+ }
+ } else if (barRect.right > x) {
+ bar.dataset.highlighted = "false"
+ bar.dataset.touched = "false"
+ if (touchedIndex >= 0) {
+ const diff = i - touchedIndex
+ if (diff === 1) {
+ bar.dataset.touchProximity = "close"
+ } else if (diff === 2) {
+ bar.dataset.touchProximity = "medium"
+ } else if (diff === 3) {
+ bar.dataset.touchProximity = "far"
+ } else {
+ delete bar.dataset.touchProximity
+ }
+ } else {
+ delete bar.dataset.touchProximity
+ }
+ } else {
+ bar.dataset.touched = "false"
+ bar.dataset.highlighted = "false"
+ delete bar.dataset.touchProximity
+ }
+ }
+
+ if (touchedIndex === -1) {
+ const firstElement = barRefs.current[barRefs.current.length - 1]
+ const lastElement = barRefs.current[0]
+ if (lastElement && x > lastElement.getBoundingClientRect().right) {
+ lastElement.dataset.thumb = "true"
+ requestBrightnessStepChange(LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)
+ } else if (firstElement && x < firstElement.getBoundingClientRect().left) {
+ firstElement.dataset.thumb = "true"
+ requestBrightnessStepChange(0)
+ }
+ }
+ }
+ })
+
+ return (
+
+
+ {Array.from({ length: LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT }).map((_, index) => {
+ const highlighted = index > initialHighlightIndexStart
+ return (
+
{
+ barRefs.current[index] = ref
+ }}
+ // biome-ignore lint/suspicious/noArrayIndexKey:
+ key={index}
+ className="transition-all transition-75 w-[2px] h-[2px] bg-neutral-400 rounded-full data-[highlighted=true]:h-2 data-[touch-proximity=close]:h-6 data-[touch-proximity=medium]:h-4 data-[touch-proximity=far]:h-2 data-[highlighted=true]:bg-teal-500 data-[touched=true]:h-8 data-[touched=true]:w-1 data-[touched=true]:bg-teal-500 data-[touched=true]:transition-none data-[prev-touched=true]:transition-none data-[thumb=true]:h-8 data-[thumb=true]:bg-teal-500"
+ />
+ )
+ })}
+
+
+
{DEVICE_FRIENDLY_NAMES[deviceName]}
+
+
+
+ )
+}
+
+function BrightnessLevelLabel({ deviceName }: { deviceName: ZigbeeDeviceName }) {
+ const currentBrightnessStep = useAtomValue(useAtomValue(brightnessStepAtoms)[deviceName])
+ const intermediateBrightnessStep = useAtomValue(useAtomValue(intermediateBrightnessStepAtoms)[deviceName])
+
+ const step = intermediateBrightnessStep === -1 ? currentBrightnessStep : intermediateBrightnessStep
+
+ let label: string
+ if (step === 0) {
+ label = "OFF"
+ } else {
+ // Convert step to percentage: step 1 = ~2%, step 43 = 100%
+ const brightnessPercentage = Math.round((step / (LIGHT_CONTROL_TILE_SLIDER_BAR_COUNT - 1)) * 100)
+ label = `${brightnessPercentage}%`
+ }
+
+ return (
+
+ {label}
+
+ )
+}
+
+export function LightSceneTile({
+ className,
+ onSceneChange,
+}: { className?: string; onSceneChange: (scene: LightSceneConfig) => void }) {
+ const [activeSceneId, setActiveSceneId] = useAtom(sceneAtom)
+ return (
+
+ {Object.entries(DEFAULT_SCENES).map(([id, { icon: Icon, name }]) => (
+
+ ))}
+
+ )
+}
diff --git a/packages/zigbee/index.ts b/packages/zigbee/index.ts
index 64f2ca0..7a5f1f0 100644
--- a/packages/zigbee/index.ts
+++ b/packages/zigbee/index.ts
@@ -12,16 +12,7 @@ export type ZigbeeDeviceStates = {
}
[ZIGBEE_DEVICE.livingRoomFloorLamp]: {
brightness: number
- level_config: {
- on_level: "previous"
- }
- linkquality: number
state: "ON" | "OFF"
- update: {
- installed_version: number
- latest_version: number
- state: "available" | "idle"
- }
}
}