├── .docker ├── resolver.conf.template └── scripts │ └── 100-envsubst-on-app-envs.sh ├── .vscode ├── extensions.json └── settings.json ├── src ├── images │ ├── favicon.ico │ ├── favicon-96x96.png │ ├── apple-touch-icon.png │ ├── generic-zigbee-device.png │ ├── web-app-manifest-192x192.png │ ├── web-app-manifest-512x512.png │ └── site.webmanifest ├── components │ ├── pickers │ │ ├── index.tsx │ │ ├── ScenePicker.tsx │ │ ├── EndpointPicker.tsx │ │ ├── GroupPicker.tsx │ │ ├── ClusterMultiPicker.tsx │ │ └── AttributePicker.tsx │ ├── home-page │ │ └── index.ts │ ├── features │ │ ├── Fan.tsx │ │ ├── Lock.tsx │ │ ├── Switch.tsx │ │ ├── Climate.tsx │ │ ├── NoAccessError.tsx │ │ ├── BaseViewer.tsx │ │ ├── Cover.tsx │ │ ├── Light.tsx │ │ ├── Text.tsx │ │ ├── Color.tsx │ │ ├── Enum.tsx │ │ ├── Numeric.tsx │ │ └── Binary.tsx │ ├── ScrollToTop.tsx │ ├── settings-page │ │ ├── tabs │ │ │ └── Bridge.tsx │ │ └── Stats.tsx │ ├── value-decorators │ │ ├── Json.tsx │ │ ├── Lqi.tsx │ │ ├── Duration.tsx │ │ ├── VendorLink.tsx │ │ ├── Countdown.tsx │ │ ├── TimeAgo.tsx │ │ ├── Availability.tsx │ │ ├── LastSeen.tsx │ │ ├── ModelLink.tsx │ │ ├── OtaLink.tsx │ │ └── DisplayValue.tsx │ ├── device-page │ │ ├── tabs │ │ │ ├── State.tsx │ │ │ ├── Scene.tsx │ │ │ ├── DeviceSpecificSettings.tsx │ │ │ ├── DeviceSettings.tsx │ │ │ ├── Exposes.tsx │ │ │ ├── Groups.tsx │ │ │ └── Clusters.tsx │ │ ├── LastLogResult.tsx │ │ ├── index.tsx │ │ └── AddToGroup.tsx │ ├── device │ │ ├── ErrorBoundary.tsx │ │ ├── index.tsx │ │ ├── LazyImage.tsx │ │ ├── DeviceControlUpdateDesc.tsx │ │ └── DeviceControlEditName.tsx │ ├── ota-page │ │ ├── index.ts │ │ ├── IndeterminateCheckbox.tsx │ │ ├── OtaUpdating.tsx │ │ └── OtaFileVersion.tsx │ ├── InfoAlert.tsx │ ├── dashboard-page │ │ ├── index.tsx │ │ ├── DashboardFeatureWrapper.tsx │ │ └── DashboardItem.tsx │ ├── Button.tsx │ ├── editors │ │ ├── TextEditor.tsx │ │ ├── EnumEditor.tsx │ │ └── RangeEditor.tsx │ ├── modal │ │ ├── Modal.tsx │ │ └── components │ │ │ ├── SearchModal.tsx │ │ │ ├── BindingRuleModal.tsx │ │ │ ├── DialogConfirmationModal.tsx │ │ │ ├── AddInstallCodeModal.tsx │ │ │ ├── EditDeviceDescModal.tsx │ │ │ ├── RenameGroupModal.tsx │ │ │ ├── ReportingRuleModal.tsx │ │ │ └── RemoveDeviceModal.tsx │ ├── SourceSwitcher.tsx │ ├── form-fields │ │ ├── SelectField.tsx │ │ ├── TextareaField.tsx │ │ ├── InputField.tsx │ │ ├── CheckboxField.tsx │ │ ├── DebouncedInput.tsx │ │ └── CheckboxesField.tsx │ ├── table │ │ └── TableHeader.tsx │ ├── ConfirmButton.tsx │ ├── network-page │ │ ├── index.tsx │ │ ├── raw-map │ │ │ ├── SliderField.tsx │ │ │ ├── ContextMenu.tsx │ │ │ └── Legend.tsx │ │ └── raw-data │ │ │ └── RawRelationGroup.tsx │ ├── DialogDropdown.tsx │ ├── group-page │ │ ├── tabs │ │ │ ├── GroupSettings.tsx │ │ │ └── Devices.tsx │ │ └── AddDeviceToGroup.tsx │ ├── PopoverDropdown.tsx │ ├── Toasts.tsx │ ├── LanguageSwitcher.tsx │ ├── SourceDot.tsx │ ├── group │ │ └── GroupScenesTile.tsx │ └── ThemeSwitcher.tsx ├── styles │ ├── NotoSans-Regular.ttf │ └── styles.global.css ├── i18next.d.ts ├── envs.ts ├── declarations.d.ts ├── vite.d.ts ├── index.tsx ├── hooks │ ├── useReRenderTracer.ts │ ├── useSearch.ts │ └── useColumnCount.ts ├── index.html ├── localStoreConsts.ts ├── layout │ ├── NavBarContext.tsx │ └── NavBar.tsx └── stories │ └── App.stories.tsx ├── index.d.ts ├── screenshots ├── devices-t1.png ├── devices-t2.png ├── devices-t3.png ├── devices-t4.png ├── device-info.png ├── network-data.png ├── network-map.png └── device-exposes.png ├── .github ├── FUNDING.yml ├── workflows │ ├── check-i18n.yml │ ├── storybook-ghpages.yml │ ├── stale.yml │ └── i18n.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.yaml ├── dependabot.yml └── prompts │ └── review-and-refactor.prompt.md ├── .dockerignore ├── index.js ├── .storybook ├── manager-head.html ├── preview.tsx └── main.ts ├── mocks ├── bridgeState.ts ├── permitJoinResponse.ts ├── touchlinkResponse.ts ├── generateExternalDefinitionResponse.ts ├── bridgeHealth.ts ├── bridgeGroups.ts └── deviceAvailability.ts ├── Dockerfile ├── docker-compose.yml ├── tsconfig.json ├── scripts ├── override-z2m-en.mjs ├── template-iterate-i18n.mjs ├── fix-i18n-order.mjs ├── check-i18n.mjs ├── find-untranslated.mjs └── update-i18n.ts ├── README.md ├── CONTRIBUTING.md ├── vite.config.mts └── .gitignore /.docker/resolver.conf.template: -------------------------------------------------------------------------------- 1 | resolver $NGINX_LOCAL_RESOLVERS ipv6=off; -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome", "vitest.explorer"] 3 | } 4 | -------------------------------------------------------------------------------- /src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/src/images/favicon.ico -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare const windfront: { 2 | getPath: () => string; 3 | }; 4 | 5 | export default windfront; 6 | -------------------------------------------------------------------------------- /screenshots/devices-t1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/devices-t1.png -------------------------------------------------------------------------------- /screenshots/devices-t2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/devices-t2.png -------------------------------------------------------------------------------- /screenshots/devices-t3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/devices-t3.png -------------------------------------------------------------------------------- /screenshots/devices-t4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/devices-t4.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Nerivec 4 | buy_me_a_coffee: Nerivec 5 | -------------------------------------------------------------------------------- /screenshots/device-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/device-info.png -------------------------------------------------------------------------------- /screenshots/network-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/network-data.png -------------------------------------------------------------------------------- /screenshots/network-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/network-map.png -------------------------------------------------------------------------------- /src/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/src/images/favicon-96x96.png -------------------------------------------------------------------------------- /screenshots/device-exposes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/screenshots/device-exposes.png -------------------------------------------------------------------------------- /src/components/pickers/index.tsx: -------------------------------------------------------------------------------- 1 | export interface ClusterGroup { 2 | name: string; 3 | clusters: Set; 4 | } 5 | -------------------------------------------------------------------------------- /src/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/src/images/apple-touch-icon.png -------------------------------------------------------------------------------- /src/styles/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/src/styles/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .github 3 | coverage 4 | node_modules 5 | screenshots 6 | scripts 7 | test 8 | .dockerignore 9 | Dockerfile -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | export default { 4 | getPath: () => join(import.meta.dirname, "dist"), 5 | }; 6 | -------------------------------------------------------------------------------- /src/images/generic-zigbee-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/src/images/generic-zigbee-device.png -------------------------------------------------------------------------------- /src/images/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/src/images/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /src/images/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nerivec/zigbee2mqtt-windfront/HEAD/src/images/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /src/components/home-page/index.ts: -------------------------------------------------------------------------------- 1 | export const enum QuickFilter { 2 | Availability = 0, 3 | Type = 1, 4 | Lqi = 2, 5 | Disabled = 3, 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.docker/scripts/100-envsubst-on-app-envs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -ex 4 | 5 | # find the file with the template envs 6 | envs=$(ls -t /usr/share/nginx/html/assets/envs*.js | head -n1) 7 | 8 | envsubst < "$envs" > ./envs_temp 9 | cp ./envs_temp "$envs" 10 | rm ./envs_temp 11 | -------------------------------------------------------------------------------- /src/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import enTranslations from "./i18n/locales/en.json" with { type: "json" }; 2 | 3 | declare module "i18next" { 4 | interface CustomTypeOptions { 5 | enableSelector: true; 6 | defaultNS: "common"; 7 | resources: typeof enTranslations; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /mocks/bridgeState.ts: -------------------------------------------------------------------------------- 1 | import type { Zigbee2MQTTAPI } from "zigbee2mqtt"; 2 | import type { Message } from "../src/types.js"; 3 | 4 | export const BRIDGE_STATE: Message = { 5 | payload: { 6 | state: "online", 7 | }, 8 | topic: "bridge/state", 9 | }; 10 | -------------------------------------------------------------------------------- /mocks/permitJoinResponse.ts: -------------------------------------------------------------------------------- 1 | import type { ResponseMessage } from "../src/types.js"; 2 | 3 | export const PERMIT_JOIN_RESPONSE: ResponseMessage<"bridge/response/permit_join"> = { 4 | payload: { 5 | status: "ok", 6 | data: { 7 | time: 254, 8 | }, 9 | }, 10 | topic: "bridge/response/permit_join", 11 | }; 12 | -------------------------------------------------------------------------------- /src/envs.ts: -------------------------------------------------------------------------------- 1 | /** biome-ignore-all lint/suspicious/noTemplateCurlyInString: used by envsubst */ 2 | // templates replaced at runtime 3 | export const Z2M_API_URLS: NonNullable = "${Z2M_API_URLS}"; 4 | export const Z2M_API_NAMES: NonNullable = "${Z2M_API_NAMES}"; 5 | export const USE_PROXY: NonNullable = "${USE_PROXY}"; 6 | -------------------------------------------------------------------------------- /src/components/features/Fan.tsx: -------------------------------------------------------------------------------- 1 | import type { FanFeature, WithAnySubFeatures } from "../../types.js"; 2 | import FeatureSubFeatures from "./FeatureSubFeatures.js"; 3 | import type { BaseWithSubFeaturesProps } from "./index.js"; 4 | 5 | type FanProps = BaseWithSubFeaturesProps>; 6 | 7 | export default function Fan(props: FanProps) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/features/Lock.tsx: -------------------------------------------------------------------------------- 1 | import type { LockFeature, WithAnySubFeatures } from "../../types.js"; 2 | import FeatureSubFeatures from "./FeatureSubFeatures.js"; 3 | import type { BaseWithSubFeaturesProps } from "./index.js"; 4 | 5 | type LockProps = BaseWithSubFeaturesProps>; 6 | 7 | export default function Lock(props: LockProps) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.ttf" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*?url" { 12 | const value: string; 13 | export default value; 14 | } 15 | 16 | declare module "*?raw" { 17 | const value: string; 18 | export default value; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/features/Switch.tsx: -------------------------------------------------------------------------------- 1 | import type { SwitchFeature, WithAnySubFeatures } from "../../types.js"; 2 | import FeatureSubFeatures from "./FeatureSubFeatures.js"; 3 | import type { BaseWithSubFeaturesProps } from "./index.js"; 4 | 5 | type SwitchProps = BaseWithSubFeaturesProps>; 6 | 7 | export default function Switch(props: SwitchProps) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useLocation } from "react-router"; 3 | 4 | const ScrollToTop = () => { 5 | const { pathname } = useLocation(); 6 | 7 | // biome-ignore lint/correctness/useExhaustiveDependencies: specific trigger 8 | useEffect(() => { 9 | window.scrollTo(0, 0); 10 | }, [pathname]); 11 | 12 | return null; 13 | }; 14 | 15 | export default ScrollToTop; 16 | -------------------------------------------------------------------------------- /src/components/features/Climate.tsx: -------------------------------------------------------------------------------- 1 | import type { ClimateFeature, WithAnySubFeatures } from "../../types.js"; 2 | import FeatureSubFeatures from "./FeatureSubFeatures.js"; 3 | import type { BaseWithSubFeaturesProps } from "./index.js"; 4 | 5 | type ClimateProps = BaseWithSubFeaturesProps>; 6 | 7 | export default function Climate(props: ClimateProps) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ViteTypeOptions { 4 | strictImportMetaEnv: unknown; 5 | } 6 | 7 | interface ImportMetaEnv { 8 | readonly VITE_Z2M_API_URLS?: string; 9 | readonly VITE_Z2M_API_NAMES?: string; 10 | /** any value set other than template => "yes" */ 11 | readonly VITE_USE_PROXY?: string; 12 | } 13 | 14 | interface ImportMeta { 15 | readonly env: ImportMetaEnv; 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine-slim AS prod 2 | 3 | # Set to Docker default in case NGINX_ENTRYPOINT_LOCAL_RESOLVERS is not set 4 | ENV NGINX_LOCAL_RESOLVERS=127.0.0.11 5 | EXPOSE 80 6 | 7 | COPY .docker/scripts/ /docker-entrypoint.d/ 8 | COPY .docker/nginx.conf /etc/nginx/ 9 | COPY .docker/resolver.conf.template /etc/nginx/templates/ 10 | 11 | RUN chmod +x /docker-entrypoint.d/100-envsubst-on-app-envs.sh 12 | 13 | COPY dist/ /usr/share/nginx/html/ 14 | -------------------------------------------------------------------------------- /src/components/settings-page/tabs/Bridge.tsx: -------------------------------------------------------------------------------- 1 | import { useShallow } from "zustand/react/shallow"; 2 | import { useAppStore } from "../../../store.js"; 3 | import Json from "../../value-decorators/Json.js"; 4 | 5 | type BridgeProps = { sourceIdx: number }; 6 | 7 | export default function Bridge({ sourceIdx }: BridgeProps) { 8 | const bridgeInfo = useAppStore(useShallow((state) => state.bridgeInfo[sourceIdx])); 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "react-app-polyfill/stable"; 2 | import { createRoot } from "react-dom/client"; 3 | 4 | import "./styles/styles.global.css"; 5 | import { Main } from "./Main.js"; 6 | 7 | const domNode = document.getElementById("root"); 8 | 9 | if (domNode) { 10 | createRoot(domNode).render(
); 11 | } 12 | 13 | // https://vite.dev/guide/build#load-error-handling 14 | window.addEventListener("vite:preloadError", () => { 15 | window.location.reload(); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/value-decorators/Json.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | 3 | type JsonProps = { 4 | obj: object; 5 | lines?: number; 6 | }; 7 | 8 | const Json = memo(({ obj, lines }: JsonProps) => { 9 | const jsonState = JSON.stringify(obj, null, 4); 10 | const computedLines = lines ?? Math.max(10, (jsonState.match(/\n/g) || "").length + 1); 11 | 12 | return