├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── feature-request.yml │ ├── support-request.yml │ └── wiki-change-request.yml ├── SECURITY.md ├── copilot-instructions.md ├── labeler.yml ├── release-drafter.yml ├── release.yml └── workflows │ ├── alpha-release.yml │ ├── beta-release.yml │ ├── build.yml │ ├── codeql-analysis.yml │ ├── deprecate-past-releases.yml │ ├── labeler.yml │ ├── pr-labeler.yml │ ├── release-drafter.yml │ ├── release.yml │ ├── stale.yml │ ├── validate.yml │ └── wiki-change-notification.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.js ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package-lock.json ├── package.json ├── screenshots ├── homebridge-config-ui-x-accessories.png ├── homebridge-config-ui-x-config.png ├── homebridge-config-ui-x-darkmode-accessories.png ├── homebridge-config-ui-x-darkmode-alexa-settings.png ├── homebridge-config-ui-x-darkmode-plugins.png ├── homebridge-config-ui-x-darkmode-status.png ├── homebridge-config-ui-x-docker-settings.png ├── homebridge-config-ui-x-logs.png ├── homebridge-config-ui-x-plugins.png ├── homebridge-config-ui-x-status.png └── homebridge-config-ui-x-users.png ├── scripts ├── extract-plugin-alias.js ├── lang-sync.ts ├── upgrade-install-plugin.sh └── upgrade-install.sh ├── src ├── app.controller.ts ├── app.gateway.ts ├── app.module.ts ├── app.service.ts ├── bin │ ├── base-platform.ts │ ├── fork.ts │ ├── hb-service.ts │ ├── platforms │ │ ├── darwin.ts │ │ ├── freebsd.ts │ │ ├── linux.ts │ │ └── win32.ts │ └── standalone.ts ├── core │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.dto.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── guards │ │ │ ├── admin.guard.ts │ │ │ ├── custom.guard.ts │ │ │ ├── ws-admin-guard.ts │ │ │ └── ws.guard.ts │ │ └── jwt.strategy.ts │ ├── config │ │ ├── config.interfaces.ts │ │ ├── config.module.ts │ │ ├── config.service.ts │ │ └── config.startup.ts │ ├── homebridge-ipc │ │ ├── homebridge-ipc.module.ts │ │ └── homebridge-ipc.service.ts │ ├── logger │ │ ├── logger.module.ts │ │ └── logger.service.ts │ ├── node-pty │ │ ├── node-pty.module.ts │ │ └── node-pty.service.ts │ ├── node-version.constants.ts │ ├── scheduler │ │ ├── scheduler.module.ts │ │ └── scheduler.service.ts │ └── spa │ │ └── spa.filter.ts ├── globalDefaults.ts ├── index.ts ├── main.ts ├── modules │ ├── accessories │ │ ├── accessories.controller.ts │ │ ├── accessories.dto.ts │ │ ├── accessories.gateway.ts │ │ ├── accessories.module.ts │ │ └── accessories.service.ts │ ├── backup │ │ ├── backup.controller.ts │ │ ├── backup.gateway.ts │ │ ├── backup.module.ts │ │ └── backup.service.ts │ ├── child-bridges │ │ ├── child-bridges.gateway.ts │ │ ├── child-bridges.module.ts │ │ └── child-bridges.service.ts │ ├── config-editor │ │ ├── config-editor.controller.ts │ │ ├── config-editor.module.ts │ │ └── config-editor.service.ts │ ├── custom-plugins │ │ ├── custom-plugins.module.ts │ │ ├── homebridge-deconz │ │ │ ├── homebridge-deconz.controller.ts │ │ │ ├── homebridge-deconz.module.ts │ │ │ └── homebridge-deconz.service.ts │ │ ├── homebridge-hue │ │ │ ├── homebridge-hue.controller.ts │ │ │ ├── homebridge-hue.module.ts │ │ │ └── homebridge-hue.service.ts │ │ └── plugins-settings-ui │ │ │ ├── plugins-settings-ui.controller.ts │ │ │ ├── plugins-settings-ui.gateway.ts │ │ │ ├── plugins-settings-ui.module.ts │ │ │ └── plugins-settings-ui.service.ts │ ├── log │ │ ├── log.gateway.ts │ │ ├── log.interfaces.ts │ │ ├── log.module.ts │ │ └── log.service.ts │ ├── platform-tools │ │ ├── docker │ │ │ ├── docker.controller.ts │ │ │ ├── docker.module.ts │ │ │ └── docker.service.ts │ │ ├── hb-service │ │ │ ├── hb-service.controller.ts │ │ │ ├── hb-service.dto.ts │ │ │ ├── hb-service.module.ts │ │ │ └── hb-service.service.ts │ │ ├── linux │ │ │ ├── linux.controller.ts │ │ │ ├── linux.module.ts │ │ │ └── linux.service.ts │ │ ├── platform-tools.module.ts │ │ └── terminal │ │ │ ├── terminal.controller.ts │ │ │ ├── terminal.gateway.ts │ │ │ ├── terminal.interfaces.ts │ │ │ ├── terminal.module.ts │ │ │ └── terminal.service.ts │ ├── plugins │ │ ├── plugins.controller.ts │ │ ├── plugins.dto.ts │ │ ├── plugins.gateway.ts │ │ ├── plugins.interfaces.ts │ │ ├── plugins.module.ts │ │ └── plugins.service.ts │ ├── server │ │ ├── server.controller.ts │ │ ├── server.dto.ts │ │ ├── server.module.ts │ │ └── server.service.ts │ ├── setup-wizard │ │ ├── setup-wizard.controller.ts │ │ ├── setup-wizard.gateway.ts │ │ ├── setup-wizard.guard.ts │ │ └── setup-wizard.module.ts │ ├── status │ │ ├── status.controller.ts │ │ ├── status.gateway.ts │ │ ├── status.interfaces.ts │ │ ├── status.module.ts │ │ └── status.service.ts │ └── users │ │ ├── users.controller.ts │ │ ├── users.dto.ts │ │ └── users.module.ts └── self-check.ts ├── test ├── .homebridge │ └── .gitignore ├── e2e │ ├── accessories.e2e-spec.ts │ ├── app.e2e-spec.ts │ ├── auth.e2e-spec.ts │ ├── backup.e2e-spec.ts │ ├── config-editor.e2e-spec.ts │ ├── custom-plugins.e2e-spec.ts │ ├── fastify.e2e-spec.ts │ ├── log.gateway.e2e-spec.ts │ ├── mdns-service.e2e-spec.ts │ ├── platform-tools-docker.e2e-spec.ts │ ├── platform-tools-hb-service.e2e-spec.ts │ ├── platform-tools-linux.e2e-spec.ts │ ├── platform-tools-terminal.e2e-spec.ts │ ├── plugin-settings-ui.e2e-spec.ts │ ├── plugins.e2e-spec.ts │ ├── plugins.gateway.e2e-spec.ts │ ├── server.e2e-spec.ts │ ├── setup-wizard.e2e-spec.ts │ ├── status.e2e-spec.ts │ └── users.e2e-spec.ts └── mocks │ ├── .uix-hb-service-homebridge-startup.json │ ├── .uix-secrets │ ├── accessories │ └── cachedAccessories │ ├── auth.json │ ├── config.json │ ├── persist │ ├── AccessoryInfo.67E41F0EA05D.json │ ├── IdentifierCache.67E41F0EA05D.json │ └── wallpaper.png │ ├── plugins │ ├── homebridge-mock-plugin-two │ │ ├── index.js │ │ └── package.json │ └── homebridge-mock-plugin │ │ ├── CHANGELOG.md │ │ ├── config.schema.json │ │ └── package.json │ └── startup.sh ├── tsconfig.build.json ├── tsconfig.json ├── ui ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── angular.json ├── package-lock.json ├── package.json ├── patches │ ├── @ng-formworks+bootstrap5+20.6.6.patch │ ├── @ng-formworks+core+20.6.6.patch │ └── @ng-formworks+cssframework+20.6.6.patch ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── core │ │ │ ├── accessories │ │ │ │ ├── accessories.interfaces.ts │ │ │ │ ├── accessories.module.ts │ │ │ │ ├── accessories.service.ts │ │ │ │ ├── accessory-info │ │ │ │ │ ├── accessory-info.component.html │ │ │ │ │ └── accessory-info.component.ts │ │ │ │ ├── accessory-tile │ │ │ │ │ ├── accessory-tile.component.html │ │ │ │ │ └── accessory-tile.component.ts │ │ │ │ └── types │ │ │ │ │ ├── air-purifier │ │ │ │ │ ├── air-purifier.component.html │ │ │ │ │ ├── air-purifier.component.scss │ │ │ │ │ ├── air-purifier.component.ts │ │ │ │ │ ├── air-purifier.manage.component.html │ │ │ │ │ └── air-purifier.manage.component.ts │ │ │ │ │ ├── air-quality-sensor │ │ │ │ │ ├── air-quality-sensor.component.html │ │ │ │ │ ├── air-quality-sensor.component.scss │ │ │ │ │ └── air-quality-sensor.component.ts │ │ │ │ │ ├── battery │ │ │ │ │ ├── battery.component.html │ │ │ │ │ ├── battery.component.scss │ │ │ │ │ └── battery.component.ts │ │ │ │ │ ├── carbon-dioxide-sensor │ │ │ │ │ ├── carbon-dioxide-sensor.component.html │ │ │ │ │ ├── carbon-dioxide-sensor.component.scss │ │ │ │ │ └── carbon-dioxide-sensor.component.ts │ │ │ │ │ ├── carbon-monoxide-sensor │ │ │ │ │ ├── carbon-monoxide-sensor.component.html │ │ │ │ │ ├── carbon-monoxide-sensor.component.scss │ │ │ │ │ └── carbon-monoxide-sensor.component.ts │ │ │ │ │ ├── contact-sensor │ │ │ │ │ ├── contact-sensor.component.html │ │ │ │ │ ├── contact-sensor.component.scss │ │ │ │ │ └── contact-sensor.component.ts │ │ │ │ │ ├── door │ │ │ │ │ ├── door.component.html │ │ │ │ │ ├── door.component.scss │ │ │ │ │ ├── door.component.ts │ │ │ │ │ ├── door.manage.component.html │ │ │ │ │ └── door.manage.component.ts │ │ │ │ │ ├── doorbell │ │ │ │ │ ├── doorbell.component.html │ │ │ │ │ ├── doorbell.component.ts │ │ │ │ │ ├── doorbell.manage.component.html │ │ │ │ │ └── doorbell.manage.component.ts │ │ │ │ │ ├── fan │ │ │ │ │ ├── fan.component.html │ │ │ │ │ ├── fan.component.scss │ │ │ │ │ ├── fan.component.ts │ │ │ │ │ ├── fan.manage.component.html │ │ │ │ │ └── fan.manage.component.ts │ │ │ │ │ ├── filter-maintenance │ │ │ │ │ ├── filter-maintenance.component.html │ │ │ │ │ ├── filter-maintenance.component.scss │ │ │ │ │ ├── filter-maintenance.component.ts │ │ │ │ │ ├── filter-maintenance.manage.component.html │ │ │ │ │ └── filter-maintenance.manage.component.ts │ │ │ │ │ ├── garage-door-opener │ │ │ │ │ ├── garage-door-opener.component.html │ │ │ │ │ ├── garage-door-opener.component.scss │ │ │ │ │ └── garage-door-opener.component.ts │ │ │ │ │ ├── heater-cooler │ │ │ │ │ ├── heater-cooler.component.html │ │ │ │ │ ├── heater-cooler.component.ts │ │ │ │ │ ├── heater-cooler.manage.component.html │ │ │ │ │ └── heater-cooler.manage.component.ts │ │ │ │ │ ├── humidifier-dehumidifier │ │ │ │ │ ├── humidifier-dehumidifier.component.html │ │ │ │ │ ├── humidifier-dehumidifier.component.ts │ │ │ │ │ ├── humidifier-dehumidifier.manage.component.html │ │ │ │ │ └── humidifier-dehumidifier.manage.component.ts │ │ │ │ │ ├── humidity-sensor │ │ │ │ │ ├── humidity-sensor.component.html │ │ │ │ │ ├── humidity-sensor.component.scss │ │ │ │ │ └── humidity-sensor.component.ts │ │ │ │ │ ├── irrigation-system │ │ │ │ │ ├── irrigation-system.component.html │ │ │ │ │ ├── irrigation-system.component.scss │ │ │ │ │ └── irrigation-system.component.ts │ │ │ │ │ ├── leak-sensor │ │ │ │ │ ├── leak-sensor.component.html │ │ │ │ │ ├── leak-sensor.component.scss │ │ │ │ │ └── leak-sensor.component.ts │ │ │ │ │ ├── light-sensor │ │ │ │ │ ├── light-sensor.component.html │ │ │ │ │ └── light-sensor.component.ts │ │ │ │ │ ├── lightbulb │ │ │ │ │ ├── lightbulb.component.html │ │ │ │ │ ├── lightbulb.component.ts │ │ │ │ │ ├── lightbulb.manage.component.html │ │ │ │ │ └── lightbulb.manage.component.ts │ │ │ │ │ ├── lock-mechanism │ │ │ │ │ ├── lock-mechanism.component.html │ │ │ │ │ ├── lock-mechanism.component.scss │ │ │ │ │ ├── lock-mechanism.component.ts │ │ │ │ │ ├── lock-mechanism.manage.component.html │ │ │ │ │ └── lock-mechanism.manage.component.ts │ │ │ │ │ ├── microphone │ │ │ │ │ ├── microphone.component.html │ │ │ │ │ ├── microphone.component.ts │ │ │ │ │ ├── microphone.manage.component.html │ │ │ │ │ └── microphone.manage.component.ts │ │ │ │ │ ├── motion-sensor │ │ │ │ │ ├── motion-sensor.component.html │ │ │ │ │ ├── motion-sensor.component.scss │ │ │ │ │ └── motion-sensor.component.ts │ │ │ │ │ ├── occupancy-sensor │ │ │ │ │ ├── occupancy-sensor.component.html │ │ │ │ │ ├── occupancy-sensor.component.scss │ │ │ │ │ └── occupancy-sensor.component.ts │ │ │ │ │ ├── outlet │ │ │ │ │ ├── outlet.component.html │ │ │ │ │ ├── outlet.component.scss │ │ │ │ │ └── outlet.component.ts │ │ │ │ │ ├── robot-vacuum │ │ │ │ │ ├── robot-vacuum.component.html │ │ │ │ │ ├── robot-vacuum.component.scss │ │ │ │ │ └── robot-vacuum.component.ts │ │ │ │ │ ├── security-system │ │ │ │ │ ├── security-system.component.html │ │ │ │ │ ├── security-system.component.scss │ │ │ │ │ ├── security-system.component.ts │ │ │ │ │ ├── security-system.manage.component.html │ │ │ │ │ └── security-system.manage.component.ts │ │ │ │ │ ├── smoke-sensor │ │ │ │ │ ├── smoke-sensor.component.html │ │ │ │ │ ├── smoke-sensor.component.scss │ │ │ │ │ └── smoke-sensor.component.ts │ │ │ │ │ ├── speaker │ │ │ │ │ ├── speaker.component.html │ │ │ │ │ ├── speaker.component.scss │ │ │ │ │ ├── speaker.component.ts │ │ │ │ │ ├── speaker.manage.component.html │ │ │ │ │ └── speaker.manage.component.ts │ │ │ │ │ ├── stateless-programmable-switch │ │ │ │ │ ├── stateless-programmable-switch.component.html │ │ │ │ │ ├── stateless-programmable-switch.component.scss │ │ │ │ │ └── stateless-programmable-switch.component.ts │ │ │ │ │ ├── switch │ │ │ │ │ ├── switch.component.html │ │ │ │ │ ├── switch.component.scss │ │ │ │ │ └── switch.component.ts │ │ │ │ │ ├── television │ │ │ │ │ ├── television.component.html │ │ │ │ │ ├── television.component.scss │ │ │ │ │ ├── television.component.ts │ │ │ │ │ ├── television.manage.component.html │ │ │ │ │ └── television.manage.component.ts │ │ │ │ │ ├── temperature-sensor │ │ │ │ │ ├── temperature-sensor.component.html │ │ │ │ │ ├── temperature-sensor.component.scss │ │ │ │ │ └── temperature-sensor.component.ts │ │ │ │ │ ├── thermostat │ │ │ │ │ ├── thermostat.component.html │ │ │ │ │ ├── thermostat.component.ts │ │ │ │ │ ├── thermostat.manage.component.html │ │ │ │ │ └── thermostat.manage.component.ts │ │ │ │ │ ├── unknown │ │ │ │ │ ├── unknown.component.html │ │ │ │ │ └── unknown.component.ts │ │ │ │ │ ├── valve │ │ │ │ │ ├── valve.component.html │ │ │ │ │ ├── valve.component.scss │ │ │ │ │ ├── valve.component.ts │ │ │ │ │ ├── valve.manage.component.html │ │ │ │ │ └── valve.manage.component.ts │ │ │ │ │ ├── washing-machine │ │ │ │ │ ├── washing-machine.component.html │ │ │ │ │ ├── washing-machine.component.scss │ │ │ │ │ └── washing-machine.component.ts │ │ │ │ │ ├── window-covering │ │ │ │ │ ├── window-covering.component.html │ │ │ │ │ ├── window-covering.component.scss │ │ │ │ │ ├── window-covering.component.ts │ │ │ │ │ ├── window-covering.manage.component.html │ │ │ │ │ └── window-covering.manage.component.ts │ │ │ │ │ └── window │ │ │ │ │ ├── window.component.html │ │ │ │ │ ├── window.component.scss │ │ │ │ │ ├── window.component.ts │ │ │ │ │ ├── window.manage.component.html │ │ │ │ │ └── window.manage.component.ts │ │ │ ├── api.service.ts │ │ │ ├── auth │ │ │ │ ├── admin.guard.ts │ │ │ │ ├── auth-helper.service.ts │ │ │ │ ├── auth.guard.ts │ │ │ │ ├── auth.interfaces.ts │ │ │ │ ├── auth.module.ts │ │ │ │ ├── auth.service.ts │ │ │ │ └── token-cache.service.ts │ │ │ ├── child-bridges.service.ts │ │ │ ├── colour.service.ts │ │ │ ├── components │ │ │ │ ├── confirm │ │ │ │ │ ├── confirm.component.html │ │ │ │ │ └── confirm.component.ts │ │ │ │ ├── information │ │ │ │ │ ├── information.component.html │ │ │ │ │ ├── information.component.scss │ │ │ │ │ └── information.component.ts │ │ │ │ ├── qrcode │ │ │ │ │ ├── qrcode.component.html │ │ │ │ │ └── qrcode.component.ts │ │ │ │ ├── restart-child-bridges │ │ │ │ │ ├── restart-child-bridges.component.html │ │ │ │ │ └── restart-child-bridges.component.ts │ │ │ │ ├── restart-homebridge │ │ │ │ │ ├── restart-homebridge.component.html │ │ │ │ │ └── restart-homebridge.component.ts │ │ │ │ ├── schema-form │ │ │ │ │ ├── schema-form.component.html │ │ │ │ │ └── schema-form.component.ts │ │ │ │ ├── spinner │ │ │ │ │ ├── spinner.component.html │ │ │ │ │ ├── spinner.component.scss │ │ │ │ │ └── spinner.component.ts │ │ │ │ └── support-banner │ │ │ │ │ ├── support-banner.component.html │ │ │ │ │ └── support-banner.component.ts │ │ │ ├── directives │ │ │ │ ├── json-schema-form-patch.directive.ts │ │ │ │ ├── long-click.directive.ts │ │ │ │ └── plugins.markdown.directive.ts │ │ │ ├── helpers │ │ │ │ └── child-bridges-schema.helper.ts │ │ │ ├── locales.ts │ │ │ ├── log.service.ts │ │ │ ├── manage-plugins │ │ │ │ ├── custom-plugins │ │ │ │ │ ├── custom-plugins.component.html │ │ │ │ │ ├── custom-plugins.component.scss │ │ │ │ │ ├── custom-plugins.component.ts │ │ │ │ │ ├── custom-plugins.module.ts │ │ │ │ │ ├── custom-plugins.service.ts │ │ │ │ │ ├── homebridge-deconz │ │ │ │ │ │ ├── homebridge-deconz.component.html │ │ │ │ │ │ └── homebridge-deconz.component.ts │ │ │ │ │ └── homebridge-hue │ │ │ │ │ │ ├── homebridge-hue.component.html │ │ │ │ │ │ └── homebridge-hue.component.ts │ │ │ │ ├── disable-plugin │ │ │ │ │ ├── disable-plugin.component.html │ │ │ │ │ └── disable-plugin.component.ts │ │ │ │ ├── donate │ │ │ │ │ ├── donate.component.html │ │ │ │ │ ├── donate.component.scss │ │ │ │ │ └── donate.component.ts │ │ │ │ ├── manage-plugin │ │ │ │ │ ├── manage-plugin.component.html │ │ │ │ │ ├── manage-plugin.component.scss │ │ │ │ │ └── manage-plugin.component.ts │ │ │ │ ├── manage-plugins.interfaces.ts │ │ │ │ ├── manage-plugins.module.ts │ │ │ │ ├── manage-plugins.service.ts │ │ │ │ ├── manage-version │ │ │ │ │ ├── manage-version.component.html │ │ │ │ │ └── manage-version.component.ts │ │ │ │ ├── manual-config │ │ │ │ │ ├── manual-config.component.html │ │ │ │ │ ├── manual-config.component.scss │ │ │ │ │ └── manual-config.component.ts │ │ │ │ ├── plugin-bridge │ │ │ │ │ ├── plugin-bridge.component.html │ │ │ │ │ ├── plugin-bridge.component.scss │ │ │ │ │ └── plugin-bridge.component.ts │ │ │ │ ├── plugin-compatibility │ │ │ │ │ ├── plugin-compatibility.component.html │ │ │ │ │ └── plugin-compatibility.component.ts │ │ │ │ ├── plugin-config │ │ │ │ │ ├── plugin-config.component.html │ │ │ │ │ ├── plugin-config.component.scss │ │ │ │ │ └── plugin-config.component.ts │ │ │ │ ├── plugin-logs │ │ │ │ │ ├── plugin-logs.component.html │ │ │ │ │ └── plugin-logs.component.ts │ │ │ │ ├── reset-accessories │ │ │ │ │ ├── reset-accessories.component.html │ │ │ │ │ └── reset-accessories.component.ts │ │ │ │ ├── switch-to-scoped │ │ │ │ │ ├── switch-to-scoped.component.html │ │ │ │ │ ├── switch-to-scoped.component.scss │ │ │ │ │ └── switch-to-scoped.component.ts │ │ │ │ └── uninstall-plugin │ │ │ │ │ ├── uninstall-plugin.component.html │ │ │ │ │ └── uninstall-plugin.component.ts │ │ │ ├── mobile-detect.service.ts │ │ │ ├── monaco-editor.service.ts │ │ │ ├── notification.service.ts │ │ │ ├── pipes │ │ │ │ ├── convert-mired.pipe.ts │ │ │ │ ├── convert-temp.pipe.ts │ │ │ │ ├── duration.pipe.ts │ │ │ │ ├── interpolate-md.pipe.ts │ │ │ │ ├── prettify.pipe.ts │ │ │ │ └── service-to-translation-string.ts │ │ │ ├── settings.interfaces.ts │ │ │ ├── settings.service.ts │ │ │ ├── terminal-navigation-guard.service.ts │ │ │ ├── terminal.service.ts │ │ │ └── ws.service.ts │ │ ├── modules │ │ │ ├── accessories │ │ │ │ ├── accessories-routing.module.ts │ │ │ │ ├── accessories.component.html │ │ │ │ ├── accessories.component.scss │ │ │ │ ├── accessories.component.ts │ │ │ │ ├── accessories.module.ts │ │ │ │ ├── accessory-support │ │ │ │ │ ├── accessory-support.component.html │ │ │ │ │ └── accessory-support.component.ts │ │ │ │ ├── add-room │ │ │ │ │ ├── add-room.component.html │ │ │ │ │ └── add-room.component.ts │ │ │ │ └── drag-here-placeholder │ │ │ │ │ ├── drag-here-placeholder.component.html │ │ │ │ │ ├── drag-here-placeholder.component.scss │ │ │ │ │ └── drag-here-placeholder.component.ts │ │ │ ├── config-editor │ │ │ │ ├── config-editor-routing.module.ts │ │ │ │ ├── config-editor.component.html │ │ │ │ ├── config-editor.component.ts │ │ │ │ ├── config-editor.interfaces.ts │ │ │ │ ├── config-editor.module.ts │ │ │ │ ├── config-editor.resolver.ts │ │ │ │ └── config-restore │ │ │ │ │ ├── config-restore.component.html │ │ │ │ │ └── config-restore.component.ts │ │ │ ├── login │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.scss │ │ │ │ ├── login.component.ts │ │ │ │ ├── login.guard.ts │ │ │ │ └── login.module.ts │ │ │ ├── logs │ │ │ │ ├── logs-routing.module.ts │ │ │ │ ├── logs.component.html │ │ │ │ ├── logs.component.ts │ │ │ │ └── logs.module.ts │ │ │ ├── platform-tools │ │ │ │ ├── docker │ │ │ │ │ ├── container-restart │ │ │ │ │ │ ├── container-restart.component.html │ │ │ │ │ │ ├── container-restart.component.scss │ │ │ │ │ │ └── container-restart.component.ts │ │ │ │ │ ├── docker-routing.module.ts │ │ │ │ │ ├── docker.module.ts │ │ │ │ │ └── startup-script │ │ │ │ │ │ ├── startup-script.component.html │ │ │ │ │ │ ├── startup-script.component.ts │ │ │ │ │ │ └── startup-script.resolver.ts │ │ │ │ ├── linux │ │ │ │ │ ├── linux-routing.module.ts │ │ │ │ │ ├── linux.module.ts │ │ │ │ │ ├── restart-linux │ │ │ │ │ │ ├── restart-linux.component.html │ │ │ │ │ │ ├── restart-linux.component.scss │ │ │ │ │ │ └── restart-linux.component.ts │ │ │ │ │ └── shutdown-linux │ │ │ │ │ │ ├── shutdown-linux.component.html │ │ │ │ │ │ └── shutdown-linux.component.ts │ │ │ │ ├── platform-tools-routing.module.ts │ │ │ │ ├── platform-tools.module.ts │ │ │ │ └── terminal │ │ │ │ │ ├── terminal-routing.module.ts │ │ │ │ │ ├── terminal.component.html │ │ │ │ │ ├── terminal.component.ts │ │ │ │ │ └── terminal.module.ts │ │ │ ├── plugins │ │ │ │ ├── plugin-card │ │ │ │ │ ├── plugin-card.component.html │ │ │ │ │ ├── plugin-card.component.ts │ │ │ │ │ └── plugin-info │ │ │ │ │ │ ├── plugin-info.component.html │ │ │ │ │ │ ├── plugin-info.component.scss │ │ │ │ │ │ └── plugin-info.component.ts │ │ │ │ ├── plugin-support │ │ │ │ │ ├── plugin-support.component.html │ │ │ │ │ └── plugin-support.component.ts │ │ │ │ ├── plugins-routing.module.ts │ │ │ │ ├── plugins.component.html │ │ │ │ ├── plugins.component.scss │ │ │ │ ├── plugins.component.ts │ │ │ │ └── plugins.module.ts │ │ │ ├── power-options │ │ │ │ ├── power-options-routing.module.ts │ │ │ │ ├── power-options.component.html │ │ │ │ ├── power-options.component.ts │ │ │ │ └── power-options.module.ts │ │ │ ├── restart │ │ │ │ ├── restart.component.html │ │ │ │ ├── restart.component.scss │ │ │ │ ├── restart.component.ts │ │ │ │ └── restart.module.ts │ │ │ ├── settings │ │ │ │ ├── accessory-control-lists │ │ │ │ │ ├── accessory-control-lists.component.html │ │ │ │ │ └── accessory-control-lists.component.ts │ │ │ │ ├── backup │ │ │ │ │ ├── backup.component.html │ │ │ │ │ ├── backup.component.ts │ │ │ │ │ ├── backup.service.ts │ │ │ │ │ └── restore │ │ │ │ │ │ ├── restore.component.html │ │ │ │ │ │ └── restore.component.ts │ │ │ │ ├── remove-all-accessories │ │ │ │ │ ├── remove-all-accessories.component.html │ │ │ │ │ └── remove-all-accessories.component.ts │ │ │ │ ├── remove-bridge-accessories │ │ │ │ │ ├── remove-bridge-accessories.component.html │ │ │ │ │ └── remove-bridge-accessories.component.ts │ │ │ │ ├── remove-individual-accessories │ │ │ │ │ ├── remove-individual-accessories.component.html │ │ │ │ │ └── remove-individual-accessories.component.ts │ │ │ │ ├── reset-all-bridges │ │ │ │ │ ├── reset-all-bridges.component.html │ │ │ │ │ └── reset-all-bridges.component.ts │ │ │ │ ├── reset-individual-bridges │ │ │ │ │ ├── reset-individual-bridges.component.html │ │ │ │ │ └── reset-individual-bridges.component.ts │ │ │ │ ├── select-network-interfaces │ │ │ │ │ ├── select-network-interfaces.component.html │ │ │ │ │ └── select-network-interfaces.component.ts │ │ │ │ ├── settings-routing.module.ts │ │ │ │ ├── settings-support │ │ │ │ │ ├── settings-support.component.html │ │ │ │ │ └── settings-support.component.ts │ │ │ │ ├── settings.component.html │ │ │ │ ├── settings.component.scss │ │ │ │ ├── settings.component.ts │ │ │ │ ├── settings.interfaces.ts │ │ │ │ ├── settings.module.ts │ │ │ │ └── wallpaper │ │ │ │ │ ├── wallpaper.component.html │ │ │ │ │ ├── wallpaper.component.scss │ │ │ │ │ └── wallpaper.component.ts │ │ │ ├── setup-wizard │ │ │ │ ├── setup-wizard-routing.module.ts │ │ │ │ ├── setup-wizard.component.html │ │ │ │ ├── setup-wizard.component.scss │ │ │ │ ├── setup-wizard.component.ts │ │ │ │ ├── setup-wizard.guard.ts │ │ │ │ └── setup-wizard.module.ts │ │ │ ├── status │ │ │ │ ├── credits │ │ │ │ │ ├── credits.component.html │ │ │ │ │ └── credits.component.ts │ │ │ │ ├── default-dashboard-layout.json │ │ │ │ ├── status.component.html │ │ │ │ ├── status.component.scss │ │ │ │ ├── status.component.ts │ │ │ │ ├── status.module.ts │ │ │ │ ├── widget-control │ │ │ │ │ ├── widget-control.component.html │ │ │ │ │ └── widget-control.component.ts │ │ │ │ ├── widget-visibility │ │ │ │ │ ├── widget-visibility.component.html │ │ │ │ │ └── widget-visibility.component.ts │ │ │ │ └── widgets │ │ │ │ │ ├── accessories-widget │ │ │ │ │ ├── accessories-widget.component.html │ │ │ │ │ └── accessories-widget.component.ts │ │ │ │ │ ├── bridges-widget │ │ │ │ │ ├── bridges-widget.component.html │ │ │ │ │ ├── bridges-widget.component.scss │ │ │ │ │ └── bridges-widget.component.ts │ │ │ │ │ ├── clock-widget │ │ │ │ │ ├── clock-widget.component.html │ │ │ │ │ └── clock-widget.component.ts │ │ │ │ │ ├── cpu-widget │ │ │ │ │ ├── cpu-widget.component.html │ │ │ │ │ ├── cpu-widget.component.scss │ │ │ │ │ └── cpu-widget.component.ts │ │ │ │ │ ├── hap-qrcode-widget │ │ │ │ │ ├── hap-qrcode-widget.component.html │ │ │ │ │ └── hap-qrcode-widget.component.ts │ │ │ │ │ ├── homebridge-logs-widget │ │ │ │ │ ├── homebridge-logs-widget.component.html │ │ │ │ │ └── homebridge-logs-widget.component.ts │ │ │ │ │ ├── memory-widget │ │ │ │ │ ├── memory-widget.component.html │ │ │ │ │ ├── memory-widget.component.scss │ │ │ │ │ └── memory-widget.component.ts │ │ │ │ │ ├── network-widget │ │ │ │ │ ├── network-widget.component.html │ │ │ │ │ ├── network-widget.component.scss │ │ │ │ │ └── network-widget.component.ts │ │ │ │ │ ├── system-info-widget │ │ │ │ │ ├── system-info-widget.component.html │ │ │ │ │ ├── system-info-widget.component.scss │ │ │ │ │ └── system-info-widget.component.ts │ │ │ │ │ ├── terminal-widget │ │ │ │ │ ├── terminal-widget.component.html │ │ │ │ │ └── terminal-widget.component.ts │ │ │ │ │ ├── update-info-widget │ │ │ │ │ ├── hb-v2-modal │ │ │ │ │ │ ├── hb-v2-modal.component.html │ │ │ │ │ │ └── hb-v2-modal.component.ts │ │ │ │ │ ├── node-version-modal │ │ │ │ │ │ ├── node-version-modal.component.html │ │ │ │ │ │ └── node-version-modal.component.ts │ │ │ │ │ ├── update-info-widget.component.html │ │ │ │ │ ├── update-info-widget.component.scss │ │ │ │ │ └── update-info-widget.component.ts │ │ │ │ │ ├── uptime-widget │ │ │ │ │ ├── uptime-widget.component.html │ │ │ │ │ └── uptime-widget.component.ts │ │ │ │ │ ├── weather-widget │ │ │ │ │ ├── weather-widget.component.html │ │ │ │ │ └── weather-widget.component.ts │ │ │ │ │ ├── widgets.component.ts │ │ │ │ │ └── widgets.interfaces.ts │ │ │ ├── support │ │ │ │ ├── support-routing.module.ts │ │ │ │ ├── support.component.html │ │ │ │ ├── support.component.ts │ │ │ │ └── support.module.ts │ │ │ └── users │ │ │ │ ├── users-2fa-disable │ │ │ │ ├── users-2fa-disable.component.html │ │ │ │ └── users-2fa-disable.component.ts │ │ │ │ ├── users-2fa-enable │ │ │ │ ├── users-2fa-enable.component.html │ │ │ │ └── users-2fa-enable.component.ts │ │ │ │ ├── users-add │ │ │ │ ├── users-add.component.html │ │ │ │ └── users-add.component.ts │ │ │ │ ├── users-edit │ │ │ │ ├── users-edit.component.html │ │ │ │ └── users-edit.component.ts │ │ │ │ ├── users-routing.module.ts │ │ │ │ ├── users-support │ │ │ │ ├── users-support.component.html │ │ │ │ └── users-support.component.ts │ │ │ │ ├── users.component.html │ │ │ │ ├── users.component.ts │ │ │ │ ├── users.interface.ts │ │ │ │ ├── users.module.ts │ │ │ │ └── users.resolver.ts │ │ └── shared │ │ │ └── layout │ │ │ ├── layout.component.html │ │ │ ├── layout.component.scss │ │ │ ├── layout.component.ts │ │ │ └── sidebar │ │ │ ├── sidebar.component.html │ │ │ ├── sidebar.component.scss │ │ │ └── sidebar.component.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── bootstrap-5 │ │ │ └── cssframework │ │ │ │ └── assets.json │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── hb-icon.png │ │ ├── homebridge-color-round.svg │ │ ├── homebridge-logo.svg │ │ ├── manifest.webmanifest │ │ └── mask-icon.svg │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── i18n │ │ ├── bg.json │ │ ├── ca.json │ │ ├── cs.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fi.json │ │ ├── fr.json │ │ ├── he.json │ │ ├── hu.json │ │ ├── id.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── mk.json │ │ ├── nl.json │ │ ├── no.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ ├── pt.json │ │ ├── ru.json │ │ ├── sl.json │ │ ├── sv.json │ │ ├── th.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── zh-CN.json │ │ └── zh-TW.json │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── scss │ │ ├── base │ │ │ ├── background.scss │ │ │ ├── buttons.scss │ │ │ ├── checkbox.scss │ │ │ ├── form.scss │ │ │ ├── layout.scss │ │ │ └── modal.scss │ │ ├── components │ │ │ ├── accessories.scss │ │ │ ├── editor.scss │ │ │ ├── json-schema-form.scss │ │ │ ├── sliding-checkbox.scss │ │ │ ├── terminal.scss │ │ │ ├── toastr.scss │ │ │ └── widgets.scss │ │ ├── styles.scss │ │ └── themes │ │ │ ├── themes-dark.scss │ │ │ └── themes-light.scss │ └── typings.d.ts ├── tsconfig.app.json └── tsconfig.json └── vitest.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: oznu 4 | custom: https://paypal.me/oznu 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Homebridge Discord Community 4 | url: https://discord.gg/C87Pvq3 5 | about: Join the Official Homebridge Discord community and ask in the ui channel. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to contribute to this project! 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: Feature Description 13 | description: | 14 | Please provide an overview of the what feature you'd like to see. 15 | 16 | * What is happening? 17 | * What you expect to happen? 18 | * What problems would this new feature solve? 19 | placeholder: | 20 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 21 | validations: 22 | required: true 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/wiki-change-request.yml: -------------------------------------------------------------------------------- 1 | name: Wiki Change Request 2 | description: want change? 3 | labels: [wiki change request] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please read the following before you start filling out this form: 9 | 10 | * This form is for requesting changes to the Homebridge Organization wiki pages only. 11 | - type: textarea 12 | id: proposed-change 13 | attributes: 14 | label: Proposed Change 15 | description: | 16 | Please describe the change you would like to see made to the wiki page. 17 | 18 | If you are requesting a new page, please describe the page you would like to see created. 19 | 20 | placeholder: | 21 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 22 | validations: 23 | required: true 24 | - type: input 25 | id: wiki-page 26 | attributes: 27 | label: Wiki Page Link 28 | description: | 29 | Please provide a link to the wiki page you would like to see changed. 30 | 31 | If you are requesting a new page, please provide details of where you would like to see this page linked from. 32 | validations: 33 | required: true 34 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'beta' label to any PR where the base branch name starts with `beta` or has a `beta` section in the name 2 | beta: 3 | - base-branch: [^beta, beta, 'beta*'] 4 | 5 | # Add 'beta' label to any PR where the base branch name starts with `beta` or has a `beta` section in the name 6 | alpha: 7 | - base-branch: [^alpha, alpha, 'alpha*'] 8 | 9 | # Add 'latest' label to any PR where the base branch name starts with `latest` or has a `latest` section in the name 10 | latest: 11 | - base-branch: [^latest, latest, 'latest*'] 12 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: v$RESOLVED_VERSION 2 | tag-template: v$RESOLVED_VERSION 3 | 4 | categories: 5 | - title: Breaking Changes 6 | labels: 7 | - breaking change 8 | - title: Featured Changes 9 | labels: 10 | - feature 11 | - enhancement 12 | - title: Bug Fixes 13 | labels: 14 | - fix 15 | - bugfix 16 | - bug 17 | - title: Other Changes 18 | labels: 19 | - documentation 20 | 21 | autolabeler: 22 | - label: fix 23 | branch: 24 | - '/fix\/.+/' 25 | title: 26 | - /fix/i 27 | - label: feature 28 | branch: 29 | - '/feature\/.+/' 30 | 31 | change-template: '- $TITLE @$AUTHOR [#$NUMBER]' 32 | template: | 33 | ## Other Changes 34 | $CHANGES 35 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # The GitHub release configuration file: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes 2 | 3 | changelog: 4 | categories: 5 | - title: Breaking Changes 🛠 6 | labels: 7 | - breaking change 8 | - title: Featured Changes ✨ 9 | labels: 10 | - feature 11 | - enhancement 12 | - title: Bug Fixes 🐛 13 | labels: 14 | - fix 15 | - bugfix 16 | - bug 17 | - title: Other Changes 18 | labels: 19 | - chore 20 | - housekeeping 21 | - '*' 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node Build 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | lint: 8 | uses: homebridge/.github/.github/workflows/eslint.yml@latest 9 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [latest, beta*] 6 | pull_request: 7 | branches: [latest, beta*] 8 | types: [review_requested, ready_for_review] 9 | schedule: 10 | - cron: '17 9 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | uses: homebridge/.github/.github/workflows/codeql-analysis.yml@latest 15 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | 3 | on: 4 | pull_request_target: # required for auto labeler 5 | types: [opened, reopened, synchronize] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | uses: homebridge/.github/.github/workflows/labeler.yml@latest 11 | secrets: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | 3 | on: 4 | pull_request: # required for auto labeler 5 | types: [opened, reopened, synchronize] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | uses: homebridge/.github/.github/workflows/pr-labeler.yml@latest 11 | secrets: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: [latest] 6 | pull_request: # required for autolabeler 7 | branches: [latest] 8 | types: [opened, reopened, synchronize, ready_for_review, review_requested] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | stale: 13 | uses: homebridge/.github/.github/workflows/release-drafter.yml@latest 14 | secrets: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '45 11 * * *' 7 | 8 | jobs: 9 | stale: 10 | uses: homebridge/.github/.github/workflows/stale.yml@latest 11 | secrets: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/wiki-change-notification.yml: -------------------------------------------------------------------------------- 1 | name: Wiki Changed Discord Notifications 2 | 3 | on: 4 | gollum 5 | 6 | jobs: 7 | notify: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: oznu/gh-wiki-edit-discord-notification@main 11 | with: 12 | discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_WIKI_EDIT }} 13 | ignore-collaborators: true 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dist 2 | dist/ 3 | public/ 4 | package/ 5 | 6 | # Logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | yarn.lock 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | .DS_Store 65 | 66 | # webstorm/intellij 67 | .idea 68 | 69 | # vscode 70 | .vscode/arduino.json 71 | .vscode/c_cpp_properties.json 72 | 73 | # test directories 74 | test/.homebridge* 75 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.insertSpaces": true, 4 | "files.eol": "\n", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit" 7 | }, 8 | "editor.formatOnSave": true, 9 | "eslint.workingDirectories": [ 10 | ".", 11 | "./ui" 12 | ], 13 | "eslint.options": { 14 | "extensions": [ 15 | ".js", 16 | ".ts", 17 | ".html" 18 | ] 19 | }, 20 | "eslint.validate": [ 21 | "javascript", 22 | "javascriptreact", 23 | "typescript", 24 | "typescriptreact", 25 | "html" 26 | ], 27 | "angular.enable-strict-mode-prompt": false 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Homebridge 4 | Copyright (c) 2017-2023 oznu 5 | Copyright (c) 2017 Michael Kellsy 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "src/**/*.spec.ts" 8 | ], 9 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 10 | } 11 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "src/**/*.spec.ts" 8 | ], 9 | "exec": "sleep 2 && UIX_DEVELOPMENT=1 UIX_INSECURE_MODE=1 UIX_SERVICE_MODE=1 HOMEBRIDGE_CONFIG_UI_TERMINAL=1 ts-node -r tsconfig-paths/register src/bin/hb-service.ts run --stdout", 10 | "signal": "SIGTERM" 11 | } 12 | -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-accessories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-accessories.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-config.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-darkmode-accessories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-darkmode-accessories.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-darkmode-alexa-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-darkmode-alexa-settings.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-darkmode-plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-darkmode-plugins.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-darkmode-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-darkmode-status.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-docker-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-docker-settings.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-logs.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-plugins.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-status.png -------------------------------------------------------------------------------- /screenshots/homebridge-config-ui-x-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/screenshots/homebridge-config-ui-x-users.png -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common' 2 | import { ApiExcludeEndpoint } from '@nestjs/swagger' 3 | 4 | import { AppService } from './app.service' 5 | 6 | @Controller() 7 | export class AppController { 8 | constructor(private readonly appService: AppService) {} 9 | 10 | @ApiExcludeEndpoint() 11 | @Get() 12 | getHello(): string { 13 | return this.appService.getHello() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app.gateway.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { WebSocketGateway } from '@nestjs/websockets' 3 | 4 | import { WsGuard } from './core/auth/guards/ws.guard' 5 | 6 | @UseGuards(WsGuard) 7 | @WebSocketGateway({ 8 | namespace: 'app', 9 | allowEIO3: true, 10 | cors: { 11 | origin: ['http://localhost:8080', 'http://localhost:4200'], 12 | credentials: true, 13 | }, 14 | }) 15 | export class AppGateway {} 16 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/bin/fork.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | process.title = 'homebridge-config-ui-x' 4 | 5 | setInterval(() => { 6 | if (!process.connected) { 7 | process.exit(1) 8 | } 9 | }, 10000) 10 | 11 | process.on('disconnect', () => { 12 | process.exit() 13 | }) 14 | 15 | import('../main') 16 | -------------------------------------------------------------------------------- /src/bin/standalone.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { homedir } from 'node:os' 3 | import { resolve } from 'node:path' 4 | import process from 'node:process' 5 | 6 | import { program } from 'commander' 7 | 8 | process.title = 'homebridge-config-ui-x' 9 | 10 | program 11 | .allowUnknownOption() 12 | .allowExcessArguments() 13 | .option('-U, --user-storage-path [path]', '', p => process.env.UIX_STORAGE_PATH = p) 14 | .option('-P, --plugin-path [path]', '', p => process.env.UIX_CUSTOM_PLUGIN_PATH = p) 15 | .option('-I, --insecure', '', () => process.env.UIX_INSECURE_MODE = '1') 16 | .option('-T, --no-timestamp', '', () => process.env.UIX_LOG_NO_TIMESTAMPS = '1') 17 | .parse(process.argv) 18 | 19 | if (!process.env.UIX_STORAGE_PATH) { 20 | process.env.UIX_STORAGE_PATH = resolve(homedir(), '.homebridge') 21 | } 22 | 23 | process.env.UIX_CONFIG_PATH = resolve(process.env.UIX_STORAGE_PATH, 'config.json') 24 | 25 | import('../main') 26 | -------------------------------------------------------------------------------- /src/core/auth/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsDefined, IsNotEmpty, IsOptional, IsString } from 'class-validator' 3 | 4 | export class AuthDto { 5 | @IsDefined() 6 | @IsString() 7 | @IsNotEmpty() 8 | @ApiProperty() 9 | readonly username: string 10 | 11 | @IsDefined() 12 | @IsString() 13 | @IsNotEmpty() 14 | @ApiProperty() 15 | readonly password: string 16 | 17 | @IsString() 18 | @IsOptional() 19 | @ApiProperty({ required: false }) 20 | readonly otp?: string 21 | } 22 | -------------------------------------------------------------------------------- /src/core/auth/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AdminGuard implements CanActivate { 5 | canActivate(context: ExecutionContext): boolean { 6 | const request = context.switchToHttp().getRequest() 7 | return request.user.admin 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/core/auth/guards/custom.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | 4 | @Injectable() 5 | export class CustomGuard extends AuthGuard('jwt') { 6 | handleRequest(err: any, user: any) { 7 | if (err || !user) { 8 | return null 9 | } 10 | return user 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/core/auth/guards/ws-admin-guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' 2 | import { verify } from 'jsonwebtoken' 3 | 4 | import { UserDto } from '../../../modules/users/users.dto' 5 | import { ConfigService } from '../../config/config.service' 6 | 7 | @Injectable() 8 | export class WsAdminGuard implements CanActivate { 9 | constructor( 10 | private configService: ConfigService, 11 | ) {} 12 | 13 | async canActivate(context: ExecutionContext) { 14 | const client = context.switchToWs().getClient() 15 | try { 16 | const user = verify(client.handshake.query.token, this.configService.secrets.secretKey) as UserDto 17 | return user.admin 18 | } catch (e) { 19 | client.disconnect() 20 | return false 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/auth/guards/ws.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' 2 | import { verify } from 'jsonwebtoken' 3 | 4 | import { UserDto } from '../../../modules/users/users.dto' 5 | import { ConfigService } from '../../config/config.service' 6 | 7 | @Injectable() 8 | export class WsGuard implements CanActivate { 9 | constructor( 10 | private configService: ConfigService, 11 | ) {} 12 | 13 | async canActivate(context: ExecutionContext) { 14 | const client = context.switchToWs().getClient() 15 | try { 16 | verify(client.handshake.query.token, this.configService.secrets.secretKey) as UserDto 17 | return true 18 | } catch (e) { 19 | client.disconnect() 20 | return false 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common' 2 | import { PassportStrategy } from '@nestjs/passport' 3 | import { ExtractJwt, Strategy } from 'passport-jwt' 4 | 5 | import { ConfigService } from '../config/config.service' 6 | import { AuthService } from './auth.service' 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy) { 10 | constructor( 11 | configService: ConfigService, 12 | private readonly authService: AuthService, 13 | ) { 14 | super({ 15 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 16 | secretOrKey: configService.secrets.secretKey, 17 | }) 18 | } 19 | 20 | async validate(payload: any) { 21 | const user = await this.authService.validateUser(payload) 22 | if (!user) { 23 | throw new UnauthorizedException() 24 | } 25 | return user 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/core/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ConfigService } from './config.service' 4 | 5 | @Module({ 6 | providers: [ConfigService], 7 | exports: [ConfigService], 8 | }) 9 | export class ConfigModule {} 10 | -------------------------------------------------------------------------------- /src/core/homebridge-ipc/homebridge-ipc.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ConfigModule } from '../config/config.module' 4 | import { LoggerModule } from '../logger/logger.module' 5 | import { HomebridgeIpcService } from './homebridge-ipc.service' 6 | 7 | @Module({ 8 | imports: [ 9 | LoggerModule, 10 | ConfigModule, 11 | ], 12 | providers: [ 13 | HomebridgeIpcService, 14 | ], 15 | exports: [ 16 | HomebridgeIpcService, 17 | ], 18 | }) 19 | export class HomebridgeIpcModule {} 20 | -------------------------------------------------------------------------------- /src/core/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { Logger } from './logger.service' 4 | 5 | @Module({ 6 | providers: [Logger], 7 | exports: [Logger], 8 | }) 9 | export class LoggerModule {} 10 | -------------------------------------------------------------------------------- /src/core/node-pty/node-pty.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { NodePtyService } from './node-pty.service' 4 | 5 | @Module({ 6 | providers: [NodePtyService], 7 | exports: [NodePtyService], 8 | }) 9 | export class NodePtyModule {} 10 | -------------------------------------------------------------------------------- /src/core/node-pty/node-pty.service.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from '@homebridge/node-pty-prebuilt-multiarch' 2 | import { Injectable } from '@nestjs/common' 3 | 4 | @Injectable() 5 | export class NodePtyService { 6 | public spawn = spawn 7 | } 8 | -------------------------------------------------------------------------------- /src/core/node-version.constants.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | /** 4 | * Node.js version and architecture compatibility constants 5 | */ 6 | 7 | /** 8 | * Architectures that support Node.js v24 9 | * Node.js v24 requires 64-bit architectures 10 | */ 11 | export const NODE_V24_SUPPORTED_ARCHITECTURES = ['x64', 'arm64', 'ppc64', 's390x'] as const 12 | 13 | /** 14 | * Check if the current architecture supports Node.js v24 15 | */ 16 | export function isNodeV24SupportedArchitecture(arch: string = process.arch): boolean { 17 | return NODE_V24_SUPPORTED_ARCHITECTURES.includes(arch as any) 18 | } 19 | -------------------------------------------------------------------------------- /src/core/scheduler/scheduler.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { SchedulerService } from './scheduler.service' 4 | 5 | @Module({ 6 | providers: [SchedulerService], 7 | exports: [SchedulerService], 8 | }) 9 | export class SchedulerModule {} 10 | -------------------------------------------------------------------------------- /src/core/scheduler/scheduler.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { cancelJob, RecurrenceRule, scheduledJobs, scheduleJob } from 'node-schedule' 3 | 4 | @Injectable() 5 | export class SchedulerService { 6 | public readonly scheduleJob = scheduleJob 7 | public readonly scheduledJobs = scheduledJobs 8 | public readonly cancelJob = cancelJob 9 | public readonly RecurrenceRule = RecurrenceRule 10 | } 11 | -------------------------------------------------------------------------------- /src/core/spa/spa.filter.ts: -------------------------------------------------------------------------------- 1 | import type { ArgumentsHost, ExceptionFilter, HttpException } from '@nestjs/common' 2 | 3 | import { resolve } from 'node:path' 4 | import process from 'node:process' 5 | 6 | import { Catch, NotFoundException } from '@nestjs/common' 7 | import { readFileSync } from 'fs-extra' 8 | 9 | @Catch(NotFoundException) 10 | export class SpaFilter implements ExceptionFilter { 11 | catch(_exception: HttpException, host: ArgumentsHost) { 12 | const ctx = host.switchToHttp() 13 | const req = ctx.getRequest() 14 | const res = ctx.getResponse() 15 | 16 | if (req.url.startsWith('/api/') || req.url.startsWith('/socket.io') || req.url.startsWith('/assets')) { 17 | return res.code(404).send('Not Found') 18 | } 19 | 20 | const file = readFileSync(resolve(process.env.UIX_BASE_PATH, 'public/index.html'), 'utf-8') 21 | res.type('text/html') 22 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate') 23 | res.header('Pragma', 'no-cache') 24 | res.header('Expires', '0') 25 | res.send(file) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/globalDefaults.ts: -------------------------------------------------------------------------------- 1 | // Global defaults for the application, shared between the ui and server 2 | globalThis.backup = { 3 | // Maximum size of a backup file in bytes 4 | maxBackupSize: 25 * 1024 * 1024, 5 | maxBackupSizeText: '25MB', 6 | 7 | // Maximum size of an individual file within backup in bytes 8 | maxBackupFileSize: 10 * 1024 * 1024, 9 | maxBackupFileSizeText: '10MB', 10 | } 11 | 12 | globalThis.terminal = { 13 | // Default buffer size for terminal output in bytes 14 | bufferSize: 50000, 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/accessories/accessories.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsDefined, IsNotEmpty, IsString } from 'class-validator' 3 | 4 | export class AccessorySetCharacteristicDto { 5 | @ApiProperty({ required: true }) 6 | @IsDefined() 7 | @IsString() 8 | characteristicType: string 9 | 10 | @ApiProperty({ required: true, type: 'string', title: 'Accepts a string, boolean, or integer value.' }) 11 | @IsDefined() 12 | @IsNotEmpty() 13 | value: string | boolean | number 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/accessories/accessories.gateway.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { SubscribeMessage, WebSocketGateway, WsException } from '@nestjs/websockets' 3 | 4 | import { WsGuard } from '../../core/auth/guards/ws.guard' 5 | import { AccessoriesService } from './accessories.service' 6 | 7 | @UseGuards(WsGuard) 8 | @WebSocketGateway({ 9 | namespace: 'accessories', 10 | allowEIO3: true, 11 | cors: { 12 | origin: ['http://localhost:8080', 'http://localhost:4200'], 13 | credentials: true, 14 | }, 15 | }) 16 | export class AccessoriesGateway { 17 | constructor( 18 | private accessoriesService: AccessoriesService, 19 | ) {} 20 | 21 | @SubscribeMessage('get-accessories') 22 | connect(client: any, payload: any) { // eslint-disable-line unused-imports/no-unused-vars 23 | this.accessoriesService.connect(client) 24 | } 25 | 26 | @SubscribeMessage('get-layout') 27 | async getAccessoryLayout(client: any, payload: any) { 28 | return await this.accessoriesService.getAccessoryLayout(payload.user) 29 | } 30 | 31 | @SubscribeMessage('save-layout') 32 | async saveAccessoryLayout(client: any, payload: any) { 33 | try { 34 | return await this.accessoriesService.saveAccessoryLayout(payload.user, payload.layout) 35 | } catch (e) { 36 | return new WsException(e) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/accessories/accessories.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../core/config/config.module' 5 | import { LoggerModule } from '../../core/logger/logger.module' 6 | import { AccessoriesController } from './accessories.controller' 7 | import { AccessoriesGateway } from './accessories.gateway' 8 | import { AccessoriesService } from './accessories.service' 9 | 10 | @Module({ 11 | imports: [ 12 | PassportModule.register({ defaultStrategy: 'jwt' }), 13 | ConfigModule, 14 | LoggerModule, 15 | ], 16 | providers: [ 17 | AccessoriesService, 18 | AccessoriesGateway, 19 | ], 20 | exports: [ 21 | AccessoriesService, 22 | ], 23 | controllers: [ 24 | AccessoriesController, 25 | ], 26 | }) 27 | export class AccessoriesModule {} 28 | -------------------------------------------------------------------------------- /src/modules/backup/backup.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../core/config/config.module' 5 | import { HomebridgeIpcModule } from '../../core/homebridge-ipc/homebridge-ipc.module' 6 | import { LoggerModule } from '../../core/logger/logger.module' 7 | import { SchedulerModule } from '../../core/scheduler/scheduler.module' 8 | import { PluginsModule } from '../plugins/plugins.module' 9 | import { BackupController } from './backup.controller' 10 | import { BackupGateway } from './backup.gateway' 11 | import { BackupService } from './backup.service' 12 | 13 | @Module({ 14 | imports: [ 15 | PassportModule.register({ defaultStrategy: 'jwt' }), 16 | ConfigModule, 17 | PluginsModule, 18 | SchedulerModule, 19 | LoggerModule, 20 | HomebridgeIpcModule, 21 | ], 22 | providers: [ 23 | BackupService, 24 | BackupGateway, 25 | ], 26 | controllers: [ 27 | BackupController, 28 | ], 29 | }) 30 | export class BackupModule {} 31 | -------------------------------------------------------------------------------- /src/modules/child-bridges/child-bridges.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../core/config/config.module' 5 | import { HomebridgeIpcModule } from '../../core/homebridge-ipc/homebridge-ipc.module' 6 | import { LoggerModule } from '../../core/logger/logger.module' 7 | import { AccessoriesModule } from '../accessories/accessories.module' 8 | import { ChildBridgesGateway } from './child-bridges.gateway' 9 | import { ChildBridgesService } from './child-bridges.service' 10 | 11 | @Module({ 12 | imports: [ 13 | PassportModule.register({ defaultStrategy: 'jwt' }), 14 | LoggerModule, 15 | ConfigModule, 16 | AccessoriesModule, 17 | HomebridgeIpcModule, 18 | ], 19 | providers: [ 20 | ChildBridgesService, 21 | ChildBridgesGateway, 22 | ], 23 | exports: [ 24 | ChildBridgesService, 25 | ], 26 | }) 27 | export class ChildBridgesModule {} 28 | -------------------------------------------------------------------------------- /src/modules/config-editor/config-editor.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../core/config/config.module' 5 | import { LoggerModule } from '../../core/logger/logger.module' 6 | import { SchedulerModule } from '../../core/scheduler/scheduler.module' 7 | import { PluginsModule } from '../plugins/plugins.module' 8 | import { ConfigEditorController } from './config-editor.controller' 9 | import { ConfigEditorService } from './config-editor.service' 10 | 11 | @Module({ 12 | imports: [ 13 | PassportModule.register({ defaultStrategy: 'jwt' }), 14 | LoggerModule, 15 | ConfigModule, 16 | SchedulerModule, 17 | PluginsModule, 18 | ], 19 | providers: [ 20 | ConfigEditorService, 21 | ], 22 | controllers: [ 23 | ConfigEditorController, 24 | ], 25 | exports: [ 26 | ConfigEditorService, 27 | ], 28 | }) 29 | export class ConfigEditorModule {} 30 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/custom-plugins.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { HomebridgeDeconzModule } from './homebridge-deconz/homebridge-deconz.module' 4 | import { HomebridgeHueModule } from './homebridge-hue/homebridge-hue.module' 5 | import { PluginsSettingsUiModule } from './plugins-settings-ui/plugins-settings-ui.module' 6 | 7 | @Module({ 8 | imports: [ 9 | HomebridgeDeconzModule, 10 | HomebridgeHueModule, 11 | PluginsSettingsUiModule, 12 | ], 13 | controllers: [], 14 | providers: [], 15 | }) 16 | export class CustomPluginsModule {} 17 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/homebridge-deconz/homebridge-deconz.controller.ts: -------------------------------------------------------------------------------- 1 | import type { StreamableFile } from '@nestjs/common' 2 | 3 | import { Controller, Get, Header, UseGuards } from '@nestjs/common' 4 | import { AuthGuard } from '@nestjs/passport' 5 | import { ApiBearerAuth, ApiTags } from '@nestjs/swagger' 6 | 7 | import { AdminGuard } from '../../../core/auth/guards/admin.guard' 8 | import { HomebridgeDeconzService } from './homebridge-deconz.service' 9 | 10 | @ApiTags('Plugins') 11 | @ApiBearerAuth() 12 | @UseGuards(AuthGuard()) 13 | @Controller('plugins/custom-plugins/homebridge-deconz') 14 | export class HomebridgeDeconzController { 15 | constructor( 16 | private homebridgeDeconzService: HomebridgeDeconzService, 17 | ) {} 18 | 19 | @UseGuards(AdminGuard) 20 | @Get('/dump-file') 21 | @Header('Content-disposition', 'attachment; filename=homebridge-deconz.json.gz') 22 | @Header('Content-Type', 'application/json+gzip') 23 | async exchangeCredentials(): Promise { 24 | return this.homebridgeDeconzService.streamDumpFile() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/homebridge-deconz/homebridge-deconz.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../../core/config/config.module' 5 | import { LoggerModule } from '../../../core/logger/logger.module' 6 | import { HomebridgeDeconzController } from './homebridge-deconz.controller' 7 | import { HomebridgeDeconzService } from './homebridge-deconz.service' 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule.register({ defaultStrategy: 'jwt' }), 12 | ConfigModule, 13 | LoggerModule, 14 | ], 15 | providers: [ 16 | HomebridgeDeconzService, 17 | ], 18 | exports: [ 19 | ], 20 | controllers: [ 21 | HomebridgeDeconzController, 22 | ], 23 | }) 24 | export class HomebridgeDeconzModule {} 25 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/homebridge-deconz/homebridge-deconz.service.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | 3 | import { Injectable, NotFoundException, StreamableFile } from '@nestjs/common' 4 | import { createReadStream, pathExists } from 'fs-extra' 5 | 6 | import { ConfigService } from '../../../core/config/config.service' 7 | 8 | @Injectable() 9 | export class HomebridgeDeconzService { 10 | constructor( 11 | private configService: ConfigService, 12 | ) {} 13 | 14 | async streamDumpFile(): Promise { 15 | const dumpPath = resolve(this.configService.storagePath, 'homebridge-deconz.json.gz') 16 | 17 | // check file exists 18 | if (!await pathExists(dumpPath)) { 19 | throw new NotFoundException() 20 | } 21 | 22 | // Stream file to client 23 | return new StreamableFile(createReadStream(dumpPath)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/homebridge-hue/homebridge-hue.controller.ts: -------------------------------------------------------------------------------- 1 | import type { StreamableFile } from '@nestjs/common' 2 | 3 | import { Controller, Get, Header, UseGuards } from '@nestjs/common' 4 | import { AuthGuard } from '@nestjs/passport' 5 | import { ApiBearerAuth, ApiTags } from '@nestjs/swagger' 6 | 7 | import { AdminGuard } from '../../../core/auth/guards/admin.guard' 8 | import { HomebridgeHueService } from './homebridge-hue.service' 9 | 10 | @ApiTags('Plugins') 11 | @ApiBearerAuth() 12 | @UseGuards(AuthGuard()) 13 | @Controller('plugins/custom-plugins/homebridge-hue') 14 | export class HomebridgeHueController { 15 | constructor( 16 | private homebridgeHueService: HomebridgeHueService, 17 | ) {} 18 | 19 | @UseGuards(AdminGuard) 20 | @Get('/dump-file') 21 | @Header('Content-disposition', 'attachment; filename=homebridge-hue.json.gz') 22 | @Header('Content-Type', 'application/json+gzip') 23 | async exchangeCredentials(): Promise { 24 | return this.homebridgeHueService.streamDumpFile() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/homebridge-hue/homebridge-hue.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../../core/config/config.module' 5 | import { LoggerModule } from '../../../core/logger/logger.module' 6 | import { HomebridgeHueController } from './homebridge-hue.controller' 7 | import { HomebridgeHueService } from './homebridge-hue.service' 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule.register({ defaultStrategy: 'jwt' }), 12 | ConfigModule, 13 | LoggerModule, 14 | ], 15 | providers: [ 16 | HomebridgeHueService, 17 | ], 18 | exports: [ 19 | ], 20 | controllers: [ 21 | HomebridgeHueController, 22 | ], 23 | }) 24 | export class HomebridgeHueModule {} 25 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/homebridge-hue/homebridge-hue.service.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | 3 | import { Injectable, NotFoundException, StreamableFile } from '@nestjs/common' 4 | import { createReadStream, pathExists } from 'fs-extra' 5 | 6 | import { ConfigService } from '../../../core/config/config.service' 7 | 8 | @Injectable() 9 | export class HomebridgeHueService { 10 | constructor( 11 | private configService: ConfigService, 12 | ) {} 13 | 14 | async streamDumpFile(): Promise { 15 | const dumpPath = resolve(this.configService.storagePath, 'homebridge-hue.json.gz') 16 | 17 | // Check file exists 18 | if (!await pathExists(dumpPath)) { 19 | throw new NotFoundException() 20 | } 21 | 22 | // Stream file to client 23 | return new StreamableFile(createReadStream(dumpPath)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/plugins-settings-ui/plugins-settings-ui.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Query, Res } from '@nestjs/common' 2 | import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger' 3 | 4 | import { PluginsSettingsUiService } from './plugins-settings-ui.service' 5 | 6 | @ApiTags('Plugins') 7 | @Controller('plugins/settings-ui') 8 | export class PluginsSettingsUiController { 9 | constructor( 10 | private pluginSettingsUiService: PluginsSettingsUiService, 11 | ) {} 12 | 13 | @Get('/:pluginName/*') 14 | @ApiOperation({ summary: 'Returns the HTML assets for a plugin\'s custom UI' }) 15 | @ApiParam({ name: 'pluginName', type: 'string' }) 16 | async serveCustomUiAsset(@Res() reply, @Param('pluginName') pluginName, @Param('*') file, @Query('origin') origin: string, @Query('v') v?: string) { 17 | return await this.pluginSettingsUiService.serveCustomUiAsset(reply, pluginName, file, origin, v) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/plugins-settings-ui/plugins-settings-ui.gateway.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from 'node:events' 2 | 3 | import { UseGuards } from '@nestjs/common' 4 | import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' 5 | 6 | import { WsAdminGuard } from '../../../core/auth/guards/ws-admin-guard' 7 | import { PluginsSettingsUiService } from './plugins-settings-ui.service' 8 | 9 | @UseGuards(WsAdminGuard) 10 | @WebSocketGateway({ 11 | namespace: 'plugins/settings-ui', 12 | allowEIO3: true, 13 | cors: { 14 | origin: ['http://localhost:8080', 'http://localhost:4200'], 15 | credentials: true, 16 | }, 17 | }) 18 | export class PluginsSettingsUiGateway { 19 | constructor( 20 | private pluginSettingsUiService: PluginsSettingsUiService, 21 | ) {} 22 | 23 | @SubscribeMessage('start') 24 | startCustomUiHandler(client: EventEmitter, payload: string) { 25 | return this.pluginSettingsUiService.startCustomUiHandler(payload, client) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/custom-plugins/plugins-settings-ui/plugins-settings-ui.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios' 2 | import { Module } from '@nestjs/common' 3 | 4 | import { ConfigModule } from '../../../core/config/config.module' 5 | import { LoggerModule } from '../../../core/logger/logger.module' 6 | import { PluginsModule } from '../../plugins/plugins.module' 7 | import { PluginsSettingsUiController } from './plugins-settings-ui.controller' 8 | import { PluginsSettingsUiGateway } from './plugins-settings-ui.gateway' 9 | import { PluginsSettingsUiService } from './plugins-settings-ui.service' 10 | 11 | @Module({ 12 | imports: [ 13 | ConfigModule, 14 | LoggerModule, 15 | PluginsModule, 16 | HttpModule, 17 | ], 18 | providers: [ 19 | PluginsSettingsUiService, 20 | PluginsSettingsUiGateway, 21 | ], 22 | controllers: [ 23 | PluginsSettingsUiController, 24 | ], 25 | }) 26 | export class PluginsSettingsUiModule {} 27 | -------------------------------------------------------------------------------- /src/modules/log/log.gateway.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from 'node:events' 2 | 3 | import type { LogTermSize } from './log.interfaces' 4 | 5 | import { UseGuards } from '@nestjs/common' 6 | import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' 7 | 8 | import { WsGuard } from '../../core/auth/guards/ws.guard' 9 | import { LogService } from './log.service' 10 | 11 | @UseGuards(WsGuard) 12 | @WebSocketGateway({ 13 | namespace: 'log', 14 | allowEIO3: true, 15 | cors: { 16 | origin: ['http://localhost:8080', 'http://localhost:4200'], 17 | credentials: true, 18 | }, 19 | }) 20 | export class LogGateway { 21 | constructor( 22 | private logService: LogService, 23 | ) {} 24 | 25 | @SubscribeMessage('tail-log') 26 | connect(client: EventEmitter, payload: LogTermSize) { 27 | this.logService.connect(client, payload) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/log/log.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface LogTermSize { 2 | cols: number 3 | rows: number 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/log/log.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ConfigModule } from '../../core/config/config.module' 4 | import { LoggerModule } from '../../core/logger/logger.module' 5 | import { NodePtyModule } from '../../core/node-pty/node-pty.module' 6 | import { LogGateway } from './log.gateway' 7 | import { LogService } from './log.service' 8 | 9 | @Module({ 10 | imports: [ 11 | ConfigModule, 12 | LoggerModule, 13 | NodePtyModule, 14 | ], 15 | providers: [ 16 | LogService, 17 | LogGateway, 18 | ], 19 | }) 20 | export class LogModule {} 21 | -------------------------------------------------------------------------------- /src/modules/platform-tools/docker/docker.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../../core/config/config.module' 5 | import { LoggerModule } from '../../../core/logger/logger.module' 6 | import { DockerController } from './docker.controller' 7 | import { DockerService } from './docker.service' 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule.register({ defaultStrategy: 'jwt' }), 12 | ConfigModule, 13 | LoggerModule, 14 | ], 15 | providers: [ 16 | DockerService, 17 | ], 18 | controllers: [ 19 | DockerController, 20 | ], 21 | }) 22 | export class DockerModule {} 23 | -------------------------------------------------------------------------------- /src/modules/platform-tools/hb-service/hb-service.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsBoolean, IsOptional, IsString } from 'class-validator' 3 | 4 | export class HbServiceStartupSettings { 5 | @IsBoolean() 6 | @ApiProperty({ default: false, required: true }) 7 | HOMEBRIDGE_DEBUG: boolean 8 | 9 | @IsBoolean() 10 | @ApiProperty({ default: false, required: true }) 11 | HOMEBRIDGE_KEEP_ORPHANS: boolean 12 | 13 | @IsBoolean() 14 | @IsOptional() 15 | @ApiProperty({ default: true, required: true }) 16 | HOMEBRIDGE_INSECURE: boolean 17 | 18 | @IsString() 19 | @ApiProperty({ required: false }) 20 | ENV_DEBUG?: string 21 | 22 | @IsString() 23 | @IsOptional() 24 | @ApiProperty({ required: false }) 25 | ENV_NODE_OPTIONS?: string 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/platform-tools/hb-service/hb-service.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../../core/config/config.module' 5 | import { LoggerModule } from '../../../core/logger/logger.module' 6 | import { HbServiceController } from './hb-service.controller' 7 | import { HbServiceService } from './hb-service.service' 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule.register({ defaultStrategy: 'jwt' }), 12 | ConfigModule, 13 | LoggerModule, 14 | ], 15 | providers: [ 16 | HbServiceService, 17 | ], 18 | controllers: [ 19 | HbServiceController, 20 | ], 21 | }) 22 | export class HbServiceModule {} 23 | -------------------------------------------------------------------------------- /src/modules/platform-tools/linux/linux.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Put, UseGuards } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' 4 | 5 | import { AdminGuard } from '../../../core/auth/guards/admin.guard' 6 | import { LinuxService } from './linux.service' 7 | 8 | @ApiTags('Platform - Linux') 9 | @ApiBearerAuth() 10 | @UseGuards(AuthGuard()) 11 | @Controller('platform-tools/linux') 12 | export class LinuxController { 13 | constructor( 14 | private readonly linuxServer: LinuxService, 15 | ) {} 16 | 17 | @UseGuards(AdminGuard) 18 | @ApiOperation({ summary: 'Restart the host server.' }) 19 | @Put('restart-host') 20 | restartHost() { 21 | return this.linuxServer.restartHost() 22 | } 23 | 24 | @UseGuards(AdminGuard) 25 | @ApiOperation({ summary: 'Shutdown the host server.' }) 26 | @Put('shutdown-host') 27 | shutdownHost() { 28 | return this.linuxServer.shutdownHost() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/platform-tools/linux/linux.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../../core/config/config.module' 5 | import { LoggerModule } from '../../../core/logger/logger.module' 6 | import { LinuxController } from './linux.controller' 7 | import { LinuxService } from './linux.service' 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule.register({ defaultStrategy: 'jwt' }), 12 | ConfigModule, 13 | LoggerModule, 14 | ], 15 | providers: [ 16 | LinuxService, 17 | ], 18 | controllers: [ 19 | LinuxController, 20 | ], 21 | }) 22 | export class LinuxModule {} 23 | -------------------------------------------------------------------------------- /src/modules/platform-tools/platform-tools.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { DockerModule } from './docker/docker.module' 4 | import { HbServiceModule } from './hb-service/hb-service.module' 5 | import { LinuxModule } from './linux/linux.module' 6 | import { TerminalModule } from './terminal/terminal.module' 7 | 8 | @Module({ 9 | imports: [ 10 | TerminalModule, 11 | LinuxModule, 12 | DockerModule, 13 | HbServiceModule, 14 | ], 15 | }) 16 | export class PlatformToolsModule {} 17 | -------------------------------------------------------------------------------- /src/modules/platform-tools/terminal/terminal.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, UseGuards } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | 4 | import { TerminalService } from './terminal.service' 5 | 6 | @UseGuards(AuthGuard()) 7 | @Controller('platform-tools/terminal') 8 | export class TerminalController { 9 | constructor( 10 | private readonly terminalService: TerminalService, 11 | ) {} 12 | 13 | @Get('has-persistent-session') 14 | hasPersistentSession() { 15 | return { hasPersistentSession: this.terminalService.hasPersistentSession() } 16 | } 17 | 18 | @Post('destroy-persistent-session') 19 | destroyPersistentSession() { 20 | this.terminalService.destroyPersistentSession() 21 | return { success: true } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/platform-tools/terminal/terminal.gateway.ts: -------------------------------------------------------------------------------- 1 | import type { TermSize, WsEventEmitter } from './terminal.interfaces' 2 | 3 | import { UseGuards } from '@nestjs/common' 4 | import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' 5 | 6 | import { WsAdminGuard } from '../../../core/auth/guards/ws-admin-guard' 7 | import { TerminalService } from './terminal.service' 8 | 9 | @UseGuards(WsAdminGuard) 10 | @WebSocketGateway({ 11 | namespace: 'platform-tools/terminal', 12 | allowEIO3: true, 13 | cors: { 14 | origin: ['http://localhost:8080', 'http://localhost:4200'], 15 | credentials: true, 16 | }, 17 | }) 18 | export class TerminalGateway { 19 | constructor( 20 | private readonly terminalService: TerminalService, 21 | ) {} 22 | 23 | @SubscribeMessage('start-session') 24 | startTerminalSession(client: WsEventEmitter, payload: TermSize) { 25 | return this.terminalService.startSession(client, payload) 26 | } 27 | 28 | @SubscribeMessage('destroy-persistent-session') 29 | destroyPersistentSession() { 30 | return this.terminalService.destroyPersistentSession() 31 | } 32 | 33 | @SubscribeMessage('check-persistent-session') 34 | checkPersistentSession() { 35 | return this.terminalService.hasPersistentSession() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/platform-tools/terminal/terminal.interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from 'node:events' 2 | 3 | export interface TermSize { 4 | cols: number 5 | rows: number 6 | } 7 | 8 | export interface WsEventEmitter extends EventEmitter { 9 | disconnect: () => void 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/platform-tools/terminal/terminal.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../../core/config/config.module' 5 | import { LoggerModule } from '../../../core/logger/logger.module' 6 | import { NodePtyModule } from '../../../core/node-pty/node-pty.module' 7 | import { TerminalController } from './terminal.controller' 8 | import { TerminalGateway } from './terminal.gateway' 9 | import { TerminalService } from './terminal.service' 10 | 11 | @Module({ 12 | imports: [ 13 | PassportModule.register({ defaultStrategy: 'jwt' }), 14 | ConfigModule, 15 | LoggerModule, 16 | NodePtyModule, 17 | ], 18 | controllers: [ 19 | TerminalController, 20 | ], 21 | providers: [ 22 | TerminalService, 23 | TerminalGateway, 24 | ], 25 | }) 26 | export class TerminalModule {} 27 | -------------------------------------------------------------------------------- /src/modules/plugins/plugins.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsDefined, 3 | IsNotEmpty, 4 | IsNumber, 5 | IsOptional, 6 | IsString, 7 | Matches, 8 | } from 'class-validator' 9 | 10 | export class HomebridgeUpdateActionDto { 11 | @IsOptional() 12 | @IsString() 13 | version?: string 14 | 15 | @IsOptional() 16 | @IsNumber() 17 | termCols?: number 18 | 19 | @IsOptional() 20 | @IsNotEmpty() 21 | termRows?: number 22 | } 23 | 24 | export class PluginActionDto { 25 | @IsDefined() 26 | @IsNotEmpty() 27 | @IsString() 28 | @Matches(/^(@[\w-]+(\.[\w-]+)*\/)?homebridge-[\w-]+$/) 29 | name: string 30 | 31 | @IsOptional() 32 | @IsString() 33 | version?: string 34 | 35 | @IsOptional() 36 | @IsNumber() 37 | termCols?: number 38 | 39 | @IsOptional() 40 | @IsNotEmpty() 41 | termRows?: number 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/plugins/plugins.module.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from 'node:https' 2 | 3 | import { HttpModule } from '@nestjs/axios' 4 | import { Module } from '@nestjs/common' 5 | import { PassportModule } from '@nestjs/passport' 6 | 7 | import { ConfigModule } from '../../core/config/config.module' 8 | import { LoggerModule } from '../../core/logger/logger.module' 9 | import { NodePtyModule } from '../../core/node-pty/node-pty.module' 10 | import { PluginsController } from './plugins.controller' 11 | import { PluginsGateway } from './plugins.gateway' 12 | import { PluginsService } from './plugins.service' 13 | 14 | @Module({ 15 | imports: [ 16 | PassportModule.register({ defaultStrategy: 'jwt' }), 17 | HttpModule.register({ 18 | headers: { 19 | 'User-Agent': 'homebridge-config-ui-x', 20 | }, 21 | timeout: 10000, 22 | httpsAgent: new Agent({ keepAlive: true }), 23 | }), 24 | NodePtyModule, 25 | ConfigModule, 26 | LoggerModule, 27 | ], 28 | providers: [ 29 | PluginsService, 30 | PluginsGateway, 31 | ], 32 | exports: [ 33 | PluginsService, 34 | ], 35 | controllers: [ 36 | PluginsController, 37 | ], 38 | }) 39 | export class PluginsModule {} 40 | -------------------------------------------------------------------------------- /src/modules/server/server.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger' 2 | import { IsArray, IsDefined, IsIn, IsString } from 'class-validator' 3 | 4 | export class HomebridgeMdnsSettingDto { 5 | @IsString() 6 | @IsDefined() 7 | @IsIn(['avahi', 'resolved', 'ciao', 'bonjour-hap']) 8 | @ApiProperty() 9 | advertiser: 'avahi' | 'resolved' | 'ciao' | 'bonjour-hap' 10 | } 11 | 12 | export class HomebridgeNetworkInterfacesDto { 13 | @IsArray() 14 | @IsString({ each: true }) 15 | @ApiProperty() 16 | adapters: string[] 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/server/server.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { ConfigModule } from '../../core/config/config.module' 5 | import { HomebridgeIpcModule } from '../../core/homebridge-ipc/homebridge-ipc.module' 6 | import { LoggerModule } from '../../core/logger/logger.module' 7 | import { AccessoriesModule } from '../accessories/accessories.module' 8 | import { ChildBridgesModule } from '../child-bridges/child-bridges.module' 9 | import { ConfigEditorModule } from '../config-editor/config-editor.module' 10 | import { ServerController } from './server.controller' 11 | import { ServerService } from './server.service' 12 | 13 | @Module({ 14 | imports: [ 15 | PassportModule.register({ defaultStrategy: 'jwt' }), 16 | ConfigModule, 17 | LoggerModule, 18 | ConfigEditorModule, 19 | AccessoriesModule, 20 | ChildBridgesModule, 21 | HomebridgeIpcModule, 22 | ], 23 | providers: [ 24 | ServerService, 25 | ], 26 | controllers: [ 27 | ServerController, 28 | ], 29 | exports: [ 30 | ServerService, 31 | ], 32 | }) 33 | export class ServerModule {} 34 | -------------------------------------------------------------------------------- /src/modules/setup-wizard/setup-wizard.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common' 2 | import { ApiOperation, ApiTags } from '@nestjs/swagger' 3 | 4 | import { AuthService } from '../../core/auth/auth.service' 5 | import { UserDto } from '../users/users.dto' 6 | import { SetupWizardGuard } from './setup-wizard.guard' 7 | 8 | @ApiTags('Setup Wizard') 9 | @UseGuards(SetupWizardGuard) 10 | @Controller('setup-wizard') 11 | export class SetupWizardController { 12 | constructor( 13 | private authService: AuthService, 14 | ) {} 15 | 16 | @Post('/create-first-user') 17 | @ApiOperation({ 18 | summary: 'Create the first user.', 19 | description: 'This endpoint is not available after the Homebridge setup wizard is complete.', 20 | }) 21 | async setupFirstUser(@Body() body: UserDto) { 22 | return await this.authService.setupFirstUser(body) 23 | } 24 | 25 | @Get('/get-setup-wizard-token') 26 | @ApiOperation({ 27 | summary: 'Creates a auth token to be used by the setup wizard.', 28 | description: 'This endpoint is not available after the Homebridge setup wizard is complete.', 29 | }) 30 | async generateSetupWizardToken() { 31 | return await this.authService.generateSetupWizardToken() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/setup-wizard/setup-wizard.guard.ts: -------------------------------------------------------------------------------- 1 | import type { CanActivate } from '@nestjs/common' 2 | import type { Observable } from 'rxjs' 3 | 4 | import { Injectable } from '@nestjs/common' 5 | 6 | import { ConfigService } from '../../core/config/config.service' 7 | 8 | @Injectable() 9 | export class SetupWizardGuard implements CanActivate { 10 | constructor( 11 | private configService: ConfigService, 12 | ) {} 13 | 14 | canActivate(): boolean | Promise | Observable { 15 | return !this.configService.setupWizardComplete 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/setup-wizard/setup-wizard.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { AuthModule } from '../../core/auth/auth.module' 4 | import { ConfigModule } from '../../core/config/config.module' 5 | import { LoggerModule } from '../../core/logger/logger.module' 6 | import { SetupWizardController } from './setup-wizard.controller' 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule, 11 | LoggerModule, 12 | AuthModule, 13 | ], 14 | controllers: [SetupWizardController], 15 | }) 16 | export class SetupWizardModule {} 17 | -------------------------------------------------------------------------------- /src/modules/status/status.interfaces.ts: -------------------------------------------------------------------------------- 1 | export enum HomebridgeStatus { 2 | OK = 'ok', 3 | UP = 'up', 4 | DOWN = 'down', 5 | } 6 | 7 | export interface HomebridgeStatusUpdate { 8 | status: HomebridgeStatus 9 | paired?: null | boolean 10 | setupUri?: null | string 11 | name?: string 12 | username?: string 13 | pin?: string 14 | } 15 | 16 | export interface DockerRelease { 17 | tag_name: string 18 | published_at: string 19 | prerelease: boolean 20 | body: string 21 | } 22 | 23 | export interface DockerReleaseInfo { 24 | version: string 25 | publishedAt: string 26 | isPrerelease: boolean 27 | isTest: boolean 28 | testTag: 'beta' | 'test' | null 29 | isLatestStable: boolean 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/status/status.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios' 2 | import { Module } from '@nestjs/common' 3 | import { PassportModule } from '@nestjs/passport' 4 | 5 | import { ConfigModule } from '../../core/config/config.module' 6 | import { HomebridgeIpcModule } from '../../core/homebridge-ipc/homebridge-ipc.module' 7 | import { LoggerModule } from '../../core/logger/logger.module' 8 | import { ChildBridgesModule } from '../child-bridges/child-bridges.module' 9 | import { PluginsModule } from '../plugins/plugins.module' 10 | import { ServerModule } from '../server/server.module' 11 | import { StatusController } from './status.controller' 12 | import { StatusGateway } from './status.gateway' 13 | import { StatusService } from './status.service' 14 | 15 | @Module({ 16 | imports: [ 17 | PassportModule.register({ defaultStrategy: 'jwt' }), 18 | HttpModule, 19 | LoggerModule, 20 | PluginsModule, 21 | ConfigModule, 22 | ServerModule, 23 | HomebridgeIpcModule, 24 | ChildBridgesModule, 25 | ], 26 | providers: [ 27 | StatusService, 28 | StatusGateway, 29 | ], 30 | controllers: [ 31 | StatusController, 32 | ], 33 | }) 34 | export class StatusModule {} 35 | -------------------------------------------------------------------------------- /src/modules/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { PassportModule } from '@nestjs/passport' 3 | 4 | import { AuthModule } from '../../core/auth/auth.module' 5 | import { ConfigModule } from '../../core/config/config.module' 6 | import { LoggerModule } from '../../core/logger/logger.module' 7 | import { UsersController } from './users.controller' 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule.register({ defaultStrategy: 'jwt' }), 12 | ConfigModule, 13 | LoggerModule, 14 | AuthModule, 15 | ], 16 | controllers: [UsersController], 17 | }) 18 | export class UsersModule {} 19 | -------------------------------------------------------------------------------- /test/.homebridge/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /test/mocks/.uix-hb-service-homebridge-startup.json: -------------------------------------------------------------------------------- 1 | { 2 | "debugMode": false, 3 | "keepOrphans": false, 4 | "insecureMode": true, 5 | "env": {} 6 | } 7 | -------------------------------------------------------------------------------- /test/mocks/.uix-secrets: -------------------------------------------------------------------------------- 1 | {"secretKey":"98b36610d6901280f24fe028233db54e2bbf5417bf313325bd3e418c0258521d"} 2 | -------------------------------------------------------------------------------- /test/mocks/accessories/cachedAccessories: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "plugin": "homebridge-daikin-esp8266", 4 | "platform": "daikin-esp8266-platform", 5 | "context": {}, 6 | "displayName": "68C63A9FAC91", 7 | "UUID": "59c27511-d7df-4c7b-907d-d62bd700b2df", 8 | "category": 1, 9 | "services": [] 10 | } 11 | ] -------------------------------------------------------------------------------- /test/mocks/auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "username": "admin", 5 | "name": "Administrator", 6 | "hashedPassword": "fd6bcc1e4d29c51f520191425d3c70610b89139be3042f1cdc3b13d58124178e3a620c9336a3029b4156aa631158e5552a4e222f2228520c2562e9e41cd259b2", 7 | "salt": "77f01970830473410589fe6764d5c07f7a007c99d07e5992f98df21166026f2a", 8 | "admin": true 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/mocks/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge Test", 4 | "port": 51826, 5 | "pin": "874-99-441", 6 | "username": "67:E4:1F:0E:A0:5D" 7 | }, 8 | "accessories": [], 9 | "platforms": [ 10 | { 11 | "name": "Config", 12 | "port": 8080, 13 | "auth": "form", 14 | "standalone": true, 15 | "platform": "config" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/mocks/persist/AccessoryInfo.67E41F0EA05D.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "Homebridge Test", 3 | "category": 2, 4 | "pincode": "874-99-441", 5 | "signSk": "fb7ecc3daa9b495d5867b49268af7568d52c4ab708e334809932976340d7a11f25e05221aa6ad3dbf4d7213e112db08752e6d94820e49038c58bc564b688626f", 6 | "signPk": "25e05221aa6ad3dbf4d7213e112db08752e6d94820e49038c58bc564b688626f", 7 | "pairedClients": {}, 8 | "pairedClientsPermission": {}, 9 | "configVersion": 3, 10 | "configHash": "931c04550644c661cbd40132194ac76b042d13a0", 11 | "setupID": "1FAP" 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/persist/IdentifierCache.67E41F0EA05D.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache": { 3 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|nextIID": 10, 4 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|0000003E-0000-1000-8000-0026BB765291|00000014-0000-1000-8000-0026BB765291": 2, 5 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|0000003E-0000-1000-8000-0026BB765291|00000020-0000-1000-8000-0026BB765291": 3, 6 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|0000003E-0000-1000-8000-0026BB765291|00000021-0000-1000-8000-0026BB765291": 4, 7 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|0000003E-0000-1000-8000-0026BB765291|00000023-0000-1000-8000-0026BB765291": 5, 8 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|0000003E-0000-1000-8000-0026BB765291|00000030-0000-1000-8000-0026BB765291": 6, 9 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|0000003E-0000-1000-8000-0026BB765291|00000052-0000-1000-8000-0026BB765291": 7, 10 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|000000A2-0000-1000-8000-0026BB765291": 8, 11 | "f6f986a8-ae40-4f38-aa34-4b232ed91fca|000000A2-0000-1000-8000-0026BB765291|00000037-0000-1000-8000-0026BB765291": 9 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/mocks/persist/wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/test/mocks/persist/wallpaper.png -------------------------------------------------------------------------------- /test/mocks/plugins/homebridge-mock-plugin-two/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (api) => { 4 | api.registerAccessory('HomebridgeMockPluginTwo', class HomebridgeMockPlugin {}) 5 | } 6 | -------------------------------------------------------------------------------- /test/mocks/plugins/homebridge-mock-plugin-two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-mock-plugin-two", 3 | "displayName": "Homebridge Mock Plugin Two", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "This is not a real plugin", 7 | "license": "Apache-2.0", 8 | "keywords": [ 9 | "homebridge-plugin" 10 | ], 11 | "main": "./index.js", 12 | "engines": { 13 | "homebridge": ">0.4.53" 14 | }, 15 | "dependencies": {}, 16 | "devDependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /test/mocks/plugins/homebridge-mock-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Homebridge Mock Plugin Changelog 2 | 3 | This is not a real plugin. -------------------------------------------------------------------------------- /test/mocks/plugins/homebridge-mock-plugin/config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "ExampleHomebridgePlugin", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "schema": { 6 | "type": "object", 7 | "properties": { 8 | "name": { 9 | "title": "Name", 10 | "type": "string", 11 | "required": true, 12 | "default": "Example Dynamic Platform" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/mocks/plugins/homebridge-mock-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-mock-plugin", 3 | "displayName": "Homebridge Mock Plugin", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "This is not a real plugin", 7 | "license": "Apache-2.0", 8 | "keywords": [ 9 | "homebridge-plugin" 10 | ], 11 | "engines": { 12 | "homebridge": ">0.4.53" 13 | }, 14 | "dependencies": {}, 15 | "devDependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /test/mocks/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is a testing mock" 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules", 5 | "test", 6 | "**/*spec.ts", 7 | "ui", 8 | "public", 9 | "scripts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "baseUrl": "./", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "outDir": "./dist", 10 | "removeComments": true, 11 | "sourceMap": true, 12 | "esModuleInterop": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "./dist", 17 | "ui", 18 | "public" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | Chrome >= 107 9 | ChromeAndroid >= 105 10 | Edge >= 107 11 | Firefox >= 104 12 | FirefoxAndroid >= 104 13 | Safari >= 16 14 | iOS >= 16 15 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/accessories.interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { ServiceType } from '@homebridge/hap-client' 2 | 3 | export type AccessoryLayout = { 4 | name: string 5 | services: Array<{ 6 | aid: number 7 | iid: number 8 | uuid: string 9 | uniqueId: string 10 | name: string 11 | serial: string 12 | bridge: string 13 | customName?: string 14 | customType?: string 15 | hidden?: boolean 16 | onDashboard?: boolean 17 | }> 18 | }[] 19 | 20 | export type ServiceTypeX = ServiceType & { 21 | customName?: string 22 | customType?: string 23 | hidden?: boolean 24 | onDashboard?: boolean 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/accessories.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { NgxMdModule } from 'ngx-md' 3 | 4 | import { AccessoriesService } from '@/app/core/accessories/accessories.service' 5 | 6 | @NgModule({ 7 | imports: [ 8 | NgxMdModule, 9 | ], 10 | providers: [ 11 | AccessoriesService, 12 | ], 13 | }) 14 | export class AccessoriesCoreModule {} 15 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/air-purifier/air-purifier.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .purifying { 3 | svg { 4 | .air-wave { 5 | animation: wave-motion 5s ease-in-out infinite; 6 | stroke: #1976d2; 7 | stroke-opacity: 0.5; 8 | } 9 | 10 | .bottom-line { 11 | animation-delay: 0.4s; 12 | } 13 | 14 | @keyframes wave-motion { 15 | 0%, 16 | 100% { 17 | transform: translateY(0px); 18 | stroke-opacity: 0.5; 19 | } 20 | 21 | 50% { 22 | transform: translateY(2px); 23 | stroke-opacity: 1; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .fill-red { 3 | svg { 4 | .leaves { 5 | fill: #d32f2f !important; 6 | fill-opacity: 0.5 !important; 7 | } 8 | } 9 | } 10 | 11 | .fill-orange { 12 | svg { 13 | .leaves { 14 | fill: #ff9800 !important; 15 | fill-opacity: 0.5 !important; 16 | } 17 | } 18 | } 19 | 20 | .fill-green { 21 | svg { 22 | .leaves { 23 | fill: #4caf50 !important; 24 | fill-opacity: 0.5 !important; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-air-quality-sensor', 9 | templateUrl: './air-quality-sensor.component.html', 10 | styleUrls: ['./air-quality-sensor.component.scss'], 11 | standalone: true, 12 | imports: [NgClass, TranslatePipe], 13 | }) 14 | export class AirQualitySensorComponent { 15 | @Input() public service: ServiceTypeX 16 | 17 | public labels = ['Unknown', 'Excellent', 'Good', 'Fair', 'Inferior', 'Poor'] 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/battery/battery.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-battery', 9 | templateUrl: './battery.component.html', 10 | styleUrls: ['./battery.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class BatteryComponent { 18 | @Input() public service: ServiceTypeX 19 | protected readonly Math = Math 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .air-line { 5 | stroke: #d32f2f; 6 | animation: flash 1s infinite; 7 | } 8 | 9 | .type { 10 | fill: #d32f2f; 11 | animation: flash 1s infinite; 12 | } 13 | 14 | @keyframes flash { 15 | 0%, 16 | 100% { 17 | opacity: 1; 18 | } 19 | 20 | 50% { 21 | opacity: 0.2; 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-carbon-dioxide-sensor', 9 | templateUrl: './carbon-dioxide-sensor.component.html', 10 | styleUrls: ['./carbon-dioxide-sensor.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class CarbonDioxideSensorComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .air-line { 5 | stroke: #d32f2f; 6 | animation: flash 1s infinite; 7 | } 8 | 9 | .type { 10 | fill: #d32f2f; 11 | animation: flash 1s infinite; 12 | } 13 | 14 | @keyframes flash { 15 | 0%, 16 | 100% { 17 | opacity: 1; 18 | } 19 | 20 | 50% { 21 | opacity: 0.2; 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-carbon-monoxide-sensor', 9 | templateUrl: './carbon-monoxide-sensor.component.html', 10 | styleUrls: ['./carbon-monoxide-sensor.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class CarbonMonoxideSensorComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .left-sensor { 5 | fill: #ff9800; 6 | fill-opacity: 0.5; 7 | } 8 | 9 | .right-object { 10 | x: 24px; 11 | } 12 | 13 | .right-sensor { 14 | x: 24px; 15 | fill: #ff9800; 16 | fill-opacity: 0.5; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-contact-sensor', 9 | templateUrl: './contact-sensor.component.html', 10 | styleUrls: ['./contact-sensor.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class ContactSensorComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/door/door.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | svg { 3 | .outline { 4 | transition: width 3s ease; 5 | width: 12.8px; 6 | } 7 | 8 | .panel { 9 | transition: width 3s ease; 10 | width: 9.6px; 11 | } 12 | 13 | .handle { 14 | visibility: visible; 15 | opacity: 1; 16 | transition: 17 | opacity 3s ease, 18 | visibility 0s linear 3s; 19 | } 20 | } 21 | 22 | .accessory-on { 23 | svg { 24 | .outline { 25 | transition: width 3s ease; 26 | width: 3.8px; 27 | } 28 | 29 | .panel { 30 | transition: width 3s ease; 31 | width: 0.6px; 32 | } 33 | 34 | .handle { 35 | visibility: hidden; 36 | opacity: 0; 37 | transition: 38 | opacity 3s ease, 39 | visibility 0s linear 0s; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/fan/fan.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .spin { 3 | svg { 4 | g { 5 | .fan-blades { 6 | animation: fa-spin 2s infinite linear; 7 | transform-origin: 53.5% 50%; 8 | } 9 | } 10 | } 11 | } 12 | 13 | .spin-counter { 14 | svg { 15 | g { 16 | .fan-blades { 17 | animation: fa-spin-counter 2s infinite linear; 18 | transform-origin: 53.5% 50%; 19 | } 20 | } 21 | } 22 | } 23 | 24 | @keyframes fa-spin { 25 | 0% { 26 | transform: rotate(0deg); 27 | } 28 | 29 | 100% { 30 | transform: rotate(359deg); 31 | } 32 | } 33 | 34 | @keyframes fa-spin-counter { 35 | 0% { 36 | transform: rotate(0deg); 37 | } 38 | 39 | 100% { 40 | transform: rotate(-359deg); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .dirty { 3 | svg { 4 | .status { 5 | fill: #e69533 !important; 6 | } 7 | } 8 | } 9 | 10 | .replace { 11 | svg { 12 | .status { 13 | fill: #d32f2f !important; 14 | } 15 | } 16 | } 17 | 18 | body.dark-mode { 19 | svg { 20 | .back { 21 | fill: #2b2b2b !important; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, inject, Input } from '@angular/core' 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4 | import { TranslatePipe } from '@ngx-translate/core' 5 | 6 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 7 | import { FilterMaintenanceManageComponent } from '@/app/core/accessories/types/filter-maintenance/filter-maintenance.manage.component' 8 | import { LongClickDirective } from '@/app/core/directives/long-click.directive' 9 | 10 | @Component({ 11 | selector: 'app-filter-maintenance', 12 | templateUrl: './filter-maintenance.component.html', 13 | styleUrls: ['./filter-maintenance.component.scss'], 14 | standalone: true, 15 | imports: [ 16 | NgClass, 17 | TranslatePipe, 18 | LongClickDirective, 19 | ], 20 | }) 21 | export class FilterMaintenanceComponent { 22 | private $modal = inject(NgbModal) 23 | 24 | @Input() public service: ServiceTypeX 25 | @Input() public readyForControl = false 26 | 27 | public onClick() { 28 | if (!this.readyForControl) { 29 | return 30 | } 31 | 32 | const ref = this.$modal.open(FilterMaintenanceManageComponent, { 33 | size: 'md', 34 | backdrop: 'static', 35 | }) 36 | ref.componentInstance.service = this.service 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.manage.component.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/humidity-sensor/humidity-sensor.component.scss: -------------------------------------------------------------------------------- 1 | .humidity-drop { 2 | position: relative; 3 | display: inline-block; 4 | width: 50px; 5 | height: 50px; 6 | line-height: 42px; 7 | border-radius: 5% 55% 70% 55%; 8 | font-size: 14px; 9 | text-align: center; 10 | margin-bottom: 6px; 11 | margin-top: 9px; 12 | background-color: grey; 13 | color: lightgrey; 14 | transform: rotate(45deg); 15 | } 16 | 17 | .humidity-drop-collapse { 18 | @media (max-width: 575px) { 19 | width: 31px; 20 | height: 31px; 21 | line-height: 28px; 22 | font-size: 10px; 23 | margin-bottom: 7px; 24 | margin-top: 5px; 25 | 26 | .humidity-drop-text { 27 | margin-left: -2px; 28 | } 29 | } 30 | } 31 | 32 | .humidity-drop-text { 33 | transform: rotate(-45deg); 34 | margin-left: -5px; 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/humidity-sensor/humidity-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core' 2 | import { TranslatePipe } from '@ngx-translate/core' 3 | 4 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 5 | 6 | @Component({ 7 | selector: 'app-humidity-sensor', 8 | templateUrl: './humidity-sensor.component.html', 9 | styleUrls: ['./humidity-sensor.component.scss'], 10 | standalone: true, 11 | imports: [ 12 | TranslatePipe, 13 | ], 14 | }) 15 | export class HumiditySensorComponent { 16 | @Input() public service: ServiceTypeX 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/irrigation-system/irrigation-system.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .field { 5 | fill: #4caf50; 6 | } 7 | 8 | .pipe { 9 | animation: pulse 5s infinite; 10 | } 11 | 12 | @keyframes pulse { 13 | 0%, 14 | 100% { 15 | stroke: #7f7f7f; 16 | stroke-opacity: 0.5; 17 | } 18 | 19 | 50% { 20 | stroke: #1976d2; 21 | stroke-opacity: 1; 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/irrigation-system/irrigation-system.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-irrigation-system', 9 | templateUrl: './irrigation-system.component.html', 10 | styleUrls: ['./irrigation-system.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class IrrigationSystemComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/leak-sensor/leak-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .red-outline { 5 | stroke: #d32f2f; 6 | animation: flash 1s infinite; 7 | } 8 | 9 | @keyframes flash { 10 | 0%, 11 | 100% { 12 | stroke-opacity: 0.8; 13 | } 14 | 15 | 50% { 16 | stroke-opacity: 0.2; 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/leak-sensor/leak-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-leak-sensor', 9 | templateUrl: './leak-sensor.component.html', 10 | styleUrls: ['./leak-sensor.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class LeakSensorComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/light-sensor/light-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { DecimalPipe } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-light-sensor', 9 | templateUrl: './light-sensor.component.html', 10 | standalone: true, 11 | imports: [ 12 | DecimalPipe, 13 | TranslatePipe, 14 | ], 15 | }) 16 | export class LightSensorComponent { 17 | @Input() public service: ServiceTypeX 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/lock-mechanism/lock-mechanism.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | @keyframes blink { 3 | 0%, 4 | 100% { 5 | fill-opacity: 1; 6 | } 7 | 8 | 50% { 9 | fill-opacity: 0.2; 10 | } 11 | } 12 | 13 | .unlocked { 14 | svg { 15 | .shackle-locked { 16 | stroke-opacity: 0 !important; 17 | } 18 | 19 | .shackle-unlocked { 20 | stroke-opacity: 1 !important; 21 | } 22 | 23 | .keyhole { 24 | fill: #ff9800 !important; 25 | } 26 | } 27 | } 28 | 29 | .jammed { 30 | svg { 31 | .shackle-locked { 32 | stroke-opacity: 0 !important; 33 | } 34 | 35 | .shackle-unlocked { 36 | stroke-opacity: 1 !important; 37 | } 38 | 39 | .keyhole { 40 | fill: #d32f2f !important; 41 | animation: blink 1s infinite; 42 | } 43 | } 44 | } 45 | 46 | .error { 47 | svg { 48 | .shackle-locked { 49 | stroke-opacity: 0 !important; 50 | } 51 | 52 | .shackle-unlocked { 53 | stroke-opacity: 1 !important; 54 | } 55 | 56 | .keyhole { 57 | fill: #d32f2f !important; 58 | animation: blink 1s infinite; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/motion-sensor/motion-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .orange-outline { 5 | stroke: #ff9800; 6 | animation: flash 1s infinite; 7 | } 8 | 9 | @keyframes flash { 10 | 0%, 11 | 100% { 12 | stroke-opacity: 0.8; 13 | } 14 | 15 | 50% { 16 | stroke-opacity: 0.2; 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/motion-sensor/motion-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-motion-sensor', 9 | templateUrl: './motion-sensor.component.html', 10 | styleUrls: ['./motion-sensor.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class MotionSensorComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/occupancy-sensor/occupancy-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .orange-outline { 5 | stroke: #ff9800; 6 | animation: flash 1s infinite; 7 | } 8 | 9 | @keyframes flash { 10 | 0%, 11 | 100% { 12 | stroke-opacity: 0.8; 13 | } 14 | 15 | 50% { 16 | stroke-opacity: 0.2; 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/occupancy-sensor/occupancy-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-occupancy-sensor', 9 | templateUrl: './occupancy-sensor.component.html', 10 | styleUrls: ['./occupancy-sensor.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class OccupancySensorComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/outlet/outlet.component.scss: -------------------------------------------------------------------------------- 1 | .accessory-on .accessory-svg { 2 | .outer { 3 | stroke: #1976d2; 4 | stroke-opacity: 0.75; 5 | stroke-width: 2.5; 6 | } 7 | 8 | .inner { 9 | stroke: #1976d2; 10 | stroke-opacity: 0.75; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/robot-vacuum/robot-vacuum.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .motion-lines { 5 | display: inline !important; 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/robot-vacuum/robot-vacuum.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | import { LongClickDirective } from '@/app/core/directives/long-click.directive' 7 | 8 | @Component({ 9 | selector: 'app-robot-vacuum', 10 | templateUrl: './robot-vacuum.component.html', 11 | styleUrls: ['./robot-vacuum.component.scss'], 12 | standalone: true, 13 | imports: [ 14 | LongClickDirective, 15 | NgClass, 16 | TranslatePipe, 17 | ], 18 | }) 19 | export class RobotVacuumComponent { 20 | @Input() public service: ServiceTypeX 21 | @Input() public readyForControl = false 22 | 23 | public onClick() { 24 | if (!this.readyForControl) { 25 | return 26 | } 27 | 28 | if ('On' in this.service.values) { 29 | this.service.getCharacteristic('On').setValue(!this.service.values.On) 30 | } else if ('Active' in this.service.values) { 31 | this.service.getCharacteristic('Active').setValue(this.service.values.Active ? 0 : 1) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/security-system/security-system.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .home { 3 | svg { 4 | .dome { 5 | fill: #1976d2 !important; 6 | } 7 | } 8 | } 9 | 10 | .away { 11 | svg { 12 | .dome { 13 | fill: #ff9800 !important; 14 | } 15 | } 16 | } 17 | 18 | .night { 19 | svg { 20 | .dome { 21 | fill: #673ab7 !important; 22 | } 23 | } 24 | } 25 | 26 | .triggered { 27 | svg { 28 | .dome { 29 | fill: #d32f2f !important; 30 | } 31 | 32 | .ray { 33 | stroke: #d32f2f; 34 | stroke-opacity: 0.5; 35 | animation: blink 1s infinite; 36 | } 37 | 38 | @keyframes blink { 39 | 0%, 40 | 100% { 41 | stroke-opacity: 1; 42 | } 43 | 44 | 50% { 45 | stroke-opacity: 0.2; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/security-system/security-system.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, inject, Input } from '@angular/core' 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4 | import { TranslatePipe } from '@ngx-translate/core' 5 | 6 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 7 | import { SecuritySystemManageComponent } from '@/app/core/accessories/types/security-system/security-system.manage.component' 8 | import { LongClickDirective } from '@/app/core/directives/long-click.directive' 9 | 10 | @Component({ 11 | selector: 'app-security-system', 12 | templateUrl: './security-system.component.html', 13 | styleUrls: ['./security-system.component.scss'], 14 | standalone: true, 15 | imports: [ 16 | LongClickDirective, 17 | NgClass, 18 | TranslatePipe, 19 | ], 20 | }) 21 | export class SecuritySystemComponent { 22 | private $modal = inject(NgbModal) 23 | 24 | @Input() public service: ServiceTypeX 25 | @Input() public readyForControl = false 26 | 27 | public onClick() { 28 | if (!this.readyForControl) { 29 | return 30 | } 31 | 32 | const ref = this.$modal.open(SecuritySystemManageComponent, { 33 | size: 'md', 34 | backdrop: 'static', 35 | }) 36 | ref.componentInstance.service = this.service 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/smoke-sensor/smoke-sensor.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .air-line { 5 | stroke: #d32f2f; 6 | animation: flash 1s infinite; 7 | } 8 | 9 | .type { 10 | fill: #d32f2f; 11 | animation: flash 1s infinite; 12 | } 13 | 14 | @keyframes flash { 15 | 0%, 16 | 100% { 17 | opacity: 1; 18 | } 19 | 20 | 50% { 21 | opacity: 0.2; 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/smoke-sensor/smoke-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-smoke-sensor', 9 | templateUrl: './smoke-sensor.component.html', 10 | styleUrls: ['./smoke-sensor.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | NgClass, 14 | TranslatePipe, 15 | ], 16 | }) 17 | export class SmokeSensorComponent { 18 | @Input() public service: ServiceTypeX 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/speaker/speaker.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on:not(.paused):not(.muted) { 3 | svg { 4 | .wave-inner { 5 | animation: pulse-inner 2s infinite; 6 | } 7 | 8 | .wave-outer { 9 | animation: pulse-outer 2s infinite; 10 | } 11 | 12 | @keyframes pulse-inner { 13 | 0% { 14 | stroke-opacity: 0.5; 15 | } 16 | 17 | 50% { 18 | stroke-opacity: 1; 19 | } 20 | 21 | 100% { 22 | stroke-opacity: 0.5; 23 | } 24 | } 25 | 26 | @keyframes pulse-outer { 27 | 0% { 28 | stroke-opacity: 0.3; 29 | } 30 | 31 | 50% { 32 | stroke-opacity: 0.8; 33 | } 34 | 35 | 100% { 36 | stroke-opacity: 0.3; 37 | } 38 | } 39 | } 40 | } 41 | 42 | body.dark-mode { 43 | div.accessory-box:not(.accessory-on) { 44 | svg { 45 | .inner { 46 | fill: #2b2b2b !important; // Dark mode inner circle color 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .press-single { 3 | svg { 4 | .single { 5 | fill: #1976d2 !important; 6 | } 7 | } 8 | } 9 | 10 | .press-double { 11 | svg { 12 | .double { 13 | fill: #1976d2 !important; 14 | } 15 | } 16 | } 17 | 18 | .press-long { 19 | svg { 20 | .long { 21 | fill: #1976d2 !important; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | 7 | @Component({ 8 | selector: 'app-stateless-programmable-switch', 9 | templateUrl: './stateless-programmable-switch.component.html', 10 | styleUrls: ['./stateless-programmable-switch.component.scss'], 11 | standalone: true, 12 | imports: [NgClass, TranslatePipe], 13 | }) 14 | export class StatelessProgrammableSwitchComponent { 15 | @Input() public service: ServiceTypeX 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/switch/switch.component.scss: -------------------------------------------------------------------------------- 1 | .accessory-on .accessory-svg { 2 | circle { 3 | stroke: #1976d2; 4 | stroke-opacity: 0.75; 5 | stroke-width: 2.5; 6 | } 7 | 8 | line { 9 | stroke: #1976d2; 10 | stroke-opacity: 0.75; 11 | } 12 | 13 | path { 14 | fill: #1976d2; 15 | fill-opacity: 0.75; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/television/television.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | body.dark-mode { 3 | .screen { 4 | fill: #2b2b2b; 5 | stroke: #7f7f7f; 6 | } 7 | } 8 | .accessory-on { 9 | svg { 10 | .screen { 11 | animation: color-change 3s infinite alternate; // Apply color animation 12 | } 13 | 14 | .stand { 15 | stroke: #7f7f7f; 16 | stroke-opacity: 1; 17 | } 18 | } 19 | } 20 | } 21 | 22 | @keyframes color-change { 23 | 0% { 24 | fill: #4fc3f7; // Light blue 25 | } 26 | 27 | 100% { 28 | fill: #0288d1; // Dark blue 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/temperature-sensor/temperature-sensor.component.scss: -------------------------------------------------------------------------------- 1 | .temperature-circle { 2 | position: relative; 3 | display: inline-block; 4 | width: 50px; 5 | height: 50px; 6 | line-height: 50px; 7 | border-radius: 50%; 8 | font-size: 14px; 9 | text-align: center; 10 | margin-bottom: 6px; 11 | background-color: grey; 12 | color: lightgrey; 13 | } 14 | 15 | .temperature-circle-collapse { 16 | @media (max-width: 575px) { 17 | width: 31px; 18 | height: 31px; 19 | line-height: 31px; 20 | font-size: 10px; 21 | margin-bottom: 7px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/temperature-sensor/temperature-sensor.component.ts: -------------------------------------------------------------------------------- 1 | import { DecimalPipe, UpperCasePipe } from '@angular/common' 2 | import { Component, inject, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | import { ConvertTempPipe } from '@/app/core/pipes/convert-temp.pipe' 7 | import { SettingsService } from '@/app/core/settings.service' 8 | 9 | @Component({ 10 | selector: 'app-temperature-sensor', 11 | templateUrl: './temperature-sensor.component.html', 12 | styleUrls: ['./temperature-sensor.component.scss'], 13 | standalone: true, 14 | imports: [DecimalPipe, ConvertTempPipe, UpperCasePipe, TranslatePipe], 15 | }) 16 | export class TemperatureSensorComponent { 17 | private $settings = inject(SettingsService) 18 | 19 | @Input() public service: ServiceTypeX 20 | 21 | public temperatureUnits = this.$settings.env.temperatureUnits 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/unknown/unknown.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core' 2 | 3 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 4 | 5 | @Component({ 6 | selector: 'app-unknown', 7 | templateUrl: './unknown.component.html', 8 | standalone: true, 9 | }) 10 | export class UnknownComponent { 11 | @Input() public service: ServiceTypeX 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/washing-machine/washing-machine.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .accessory-on { 3 | svg { 4 | .handle { 5 | fill: #1976d2; 6 | fill-opacity: 0.5; 7 | transform-origin: 48% 53%; 8 | animation: rotate-handle 2s linear infinite; 9 | 10 | @keyframes rotate-handle { 11 | 0% { 12 | transform: rotate(0deg); 13 | } 14 | 15 | 100% { 16 | transform: rotate(360deg); 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/washing-machine/washing-machine.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, Input } from '@angular/core' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces' 6 | import { LongClickDirective } from '@/app/core/directives/long-click.directive' 7 | 8 | @Component({ 9 | selector: 'app-washing-machine', 10 | templateUrl: './washing-machine.component.html', 11 | styleUrls: ['./washing-machine.component.scss'], 12 | standalone: true, 13 | imports: [ 14 | LongClickDirective, 15 | NgClass, 16 | TranslatePipe, 17 | ], 18 | }) 19 | export class WashingMachineComponent { 20 | @Input() public service: ServiceTypeX 21 | @Input() public readyForControl = false 22 | 23 | public onClick() { 24 | if (!this.readyForControl) { 25 | return 26 | } 27 | 28 | if ('On' in this.service.values) { 29 | this.service.getCharacteristic('On').setValue(!this.service.values.On) 30 | } else if ('Active' in this.service.values) { 31 | this.service.getCharacteristic('Active').setValue(this.service.values.Active ? 0 : 1) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/window-covering/window-covering.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | body.dark-mode { 3 | .c-blinds { 4 | fill: #2b2b2b; 5 | stroke: #2b2b2b; 6 | stroke-opacity: 0.8; 7 | fill-opacity: 0.8; 8 | } 9 | } 10 | svg { 11 | .c-blinds { 12 | transition: height 3s ease; 13 | height: 27.6px; 14 | } 15 | 16 | .c-light { 17 | transition: 18 | y 3s ease, 19 | height 3s ease; 20 | y: 29.9px; 21 | height: 0; 22 | } 23 | } 24 | 25 | .accessory-on { 26 | svg { 27 | .c-blinds { 28 | transition: height 3s ease; 29 | height: 0.3px; 30 | } 31 | 32 | .c-light { 33 | transition: 34 | y 3s ease, 35 | height 3s ease; 36 | y: 2.65px; 37 | height: 27.3px; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/app/core/accessories/types/window/window.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | svg { 3 | .outline-left { 4 | x: 2.8px; 5 | width: 12.4px; 6 | } 7 | 8 | .divider-left { 9 | x: 3.66px; 10 | width: 10.68px; 11 | } 12 | 13 | .handle-left { 14 | x: 15.16px; 15 | width: 0.1px; 16 | } 17 | 18 | .outline-right { 19 | x: 16.8px; 20 | width: 12.4px; 21 | } 22 | 23 | .divider-right { 24 | x: 17.66px; 25 | width: 10.68px; 26 | } 27 | 28 | .handle-right { 29 | x: 16.76px; 30 | width: 0.1px; 31 | } 32 | } 33 | 34 | .accessory-on { 35 | svg { 36 | .outline-left { 37 | x: 2.8px; 38 | width: 2.4px; 39 | } 40 | 41 | .divider-left { 42 | x: 3.66px; 43 | width: 0.68px; 44 | } 45 | 46 | .handle-left { 47 | x: 5.16px; 48 | width: 0.1px; 49 | } 50 | 51 | .outline-right { 52 | x: 26.8px; 53 | width: 2.4px; 54 | } 55 | 56 | .divider-right { 57 | x: 27.66px; 58 | width: 0.68px; 59 | } 60 | 61 | .handle-right { 62 | x: 26.76px; 63 | width: 0.1px; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/src/app/core/api.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http' 2 | import { inject, Injectable } from '@angular/core' 3 | import { Observable } from 'rxjs' 4 | 5 | import { environment } from '@/environments/environment' 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class ApiService { 11 | private $http = inject(HttpClient) 12 | 13 | public get(url: string, options?): Observable { 14 | return this.$http.get(`${environment.api.base}${url}`, options) 15 | } 16 | 17 | public post(url: string, body: any | null, options?): Observable { 18 | return this.$http.post(`${environment.api.base}${url}`, body, options) 19 | } 20 | 21 | public put(url: string, body: any | null, options?): Observable { 22 | return this.$http.put(`${environment.api.base}${url}`, body, options) 23 | } 24 | 25 | public patch(url: string, body: any | null, options?): Observable { 26 | return this.$http.patch(`${environment.api.base}${url}`, body, options) 27 | } 28 | 29 | public delete(url: string, options?): Observable { 30 | return this.$http.delete(`${environment.api.base}${url}`, options) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/app/core/auth/auth.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface UserInterface { 2 | username?: string 3 | name?: string 4 | admin?: boolean 5 | instanceId?: string 6 | } 7 | 8 | export interface TokenCacheEntry { 9 | token: string | null 10 | timestamp: number 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/app/core/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { JwtModule } from '@auth0/angular-jwt' 3 | 4 | import { AdminGuard } from '@/app/core/auth/admin.guard' 5 | import { AuthHelperService } from '@/app/core/auth/auth-helper.service' 6 | import { AuthGuard } from '@/app/core/auth/auth.guard' 7 | import { AuthService } from '@/app/core/auth/auth.service' 8 | import { TokenCacheService } from '@/app/core/auth/token-cache.service' 9 | import { environment } from '@/environments/environment' 10 | 11 | const tokenGetter = () => localStorage.getItem(environment.jwt.tokenKey) 12 | 13 | @NgModule({ 14 | imports: [ 15 | JwtModule.forRoot({ 16 | config: { 17 | authScheme: 'bearer ', 18 | tokenGetter, 19 | skipWhenExpired: false, 20 | allowedDomains: environment.jwt.allowedDomains, 21 | disallowedRoutes: environment.jwt.disallowedRoutes, 22 | }, 23 | }), 24 | ], 25 | providers: [ 26 | AuthHelperService, 27 | AuthService, 28 | AuthGuard, 29 | AdminGuard, 30 | TokenCacheService, 31 | ], 32 | exports: [], 33 | }) 34 | class AuthModule {} 35 | 36 | // Token getter 37 | export { AuthModule, tokenGetter } 38 | -------------------------------------------------------------------------------- /ui/src/app/core/auth/token-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | 3 | import { TokenCacheEntry } from '@/app/core/auth/auth.interfaces' 4 | import { environment } from '@/environments/environment' 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class TokenCacheService { 10 | private cache: TokenCacheEntry | null = null 11 | private readonly CACHE_DURATION_MS = 60000 // 1 minute 12 | 13 | /** 14 | * Gets the token from cache or localStorage if cache is expired/empty 15 | */ 16 | public getToken(): string | null { 17 | const now = Date.now() 18 | 19 | // Check if we have valid cached token 20 | if (this.cache && (now - this.cache.timestamp) < this.CACHE_DURATION_MS) { 21 | return this.cache.token 22 | } 23 | 24 | // Cache expired or empty - read from localStorage 25 | const token = window.localStorage.getItem(environment.jwt.tokenKey) 26 | 27 | // Update cache 28 | this.cache = { 29 | token, 30 | timestamp: now, 31 | } 32 | 33 | return token 34 | } 35 | 36 | /** 37 | * Invalidates the cache - forces next getToken() to read from localStorage 38 | */ 39 | public invalidateCache(): void { 40 | this.cache = null 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/app/core/components/confirm/confirm.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, Input } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | @Component({ 6 | templateUrl: './confirm.component.html', 7 | standalone: true, 8 | imports: [TranslatePipe], 9 | }) 10 | export class ConfirmComponent { 11 | private $activeModal = inject(NgbActiveModal) 12 | 13 | @Input() title: string 14 | @Input() message: string 15 | @Input() message2?: string 16 | @Input() message3?: string 17 | @Input() confirmButtonLabel?: string 18 | @Input() confirmButtonClass?: string 19 | @Input() faIconClass?: string 20 | 21 | public dismissModal() { 22 | this.$activeModal.dismiss('Dismiss') 23 | } 24 | 25 | public closeModal() { 26 | this.$activeModal.close() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/app/core/components/information/information.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, Input } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | import { NgxMdModule } from 'ngx-md' 5 | 6 | import { PluginsMarkdownDirective } from '@/app/core/directives/plugins.markdown.directive' 7 | 8 | @Component({ 9 | templateUrl: './information.component.html', 10 | styleUrls: ['./information.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | TranslatePipe, 14 | NgxMdModule, 15 | PluginsMarkdownDirective, 16 | ], 17 | }) 18 | export class InformationComponent { 19 | private $activeModal = inject(NgbActiveModal) 20 | 21 | @Input() title: string 22 | @Input() subtitle?: string 23 | @Input() message: string 24 | @Input() message2?: string 25 | @Input() ctaButtonLabel?: string 26 | @Input() ctaButtonLink?: string 27 | @Input() faIconClass: string 28 | @Input() markdownMessage2?: string 29 | 30 | public dismissModal() { 31 | this.$activeModal.dismiss('Dismiss') 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/core/components/qrcode/qrcode.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /ui/src/app/core/components/qrcode/qrcode.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, OnChanges, viewChild } from '@angular/core' 2 | import { toString } from 'qrcode' 3 | 4 | @Component({ 5 | selector: 'app-qrcode', 6 | templateUrl: './qrcode.component.html', 7 | standalone: true, 8 | }) 9 | export class QrcodeComponent implements OnChanges { 10 | @Input() data: string 11 | 12 | private readonly qrcodeElement = viewChild('qrcode') 13 | 14 | public ngOnChanges(): void { 15 | this.renderQrCode() 16 | } 17 | 18 | private async renderQrCode() { 19 | if (this.data) { 20 | const qrcodeElement = this.qrcodeElement() 21 | qrcodeElement.nativeElement.innerHTML = await toString(this.data, { 22 | type: 'svg', 23 | margin: 0, 24 | color: { 25 | light: '#ffffff00', 26 | dark: document.body.classList.contains('dark-mode') ? '#FFF' : '#000', 27 | }, 28 | }) 29 | const svgElement = qrcodeElement.nativeElement.querySelector('svg') as SVGElement 30 | const svgPathElement = svgElement.querySelector('path') as SVGPathElement 31 | svgPathElement.classList.add('qr-code-theme-color') 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/core/components/restart-homebridge/restart-homebridge.component.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /ui/src/app/core/components/restart-homebridge/restart-homebridge.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { Router } from '@angular/router' 3 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 4 | import { TranslatePipe } from '@ngx-translate/core' 5 | 6 | @Component({ 7 | templateUrl: './restart-homebridge.component.html', 8 | standalone: true, 9 | imports: [TranslatePipe], 10 | }) 11 | export class RestartHomebridgeComponent { 12 | private $activeModal = inject(NgbActiveModal) 13 | private $router = inject(Router) 14 | 15 | public onRestartHomebridgeClick() { 16 | void this.$router.navigate(['/restart']) 17 | this.$activeModal.close() 18 | } 19 | 20 | public dismissModal() { 21 | this.$activeModal.dismiss('Dismiss') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/core/components/schema-form/schema-form.component.html: -------------------------------------------------------------------------------- 1 | 2 | @if (configSchema.schema) { 3 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /ui/src/app/core/components/spinner/spinner.component.scss: -------------------------------------------------------------------------------- 1 | .app-spinner-container { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 60px; 6 | right: 0; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | z-index: 1000; 11 | 12 | @media (max-width: 767px) { 13 | left: 0; 14 | top: 65px; 15 | } 16 | } 17 | 18 | .animate_loader svg { 19 | position: absolute; 20 | top: 0; 21 | right: 0; 22 | bottom: 0; 23 | left: 0; 24 | margin: auto; 25 | width: 200px; 26 | overflow: visible; 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/app/core/components/spinner/spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'app-spinner', 5 | templateUrl: './spinner.component.html', 6 | styleUrls: ['./spinner.component.scss'], 7 | standalone: true, 8 | }) 9 | export class SpinnerComponent {} 10 | -------------------------------------------------------------------------------- /ui/src/app/core/components/support-banner/support-banner.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /ui/src/app/core/components/support-banner/support-banner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { NgbAlert } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | @Component({ 6 | selector: 'app-support-banner', 7 | templateUrl: './support-banner.component.html', 8 | standalone: true, 9 | imports: [TranslatePipe, NgbAlert], 10 | }) 11 | export class SupportBannerComponent { 12 | public readonly linkGithub = 'GitHub' 13 | public readonly linkDiscord = 'Discord' 14 | } 15 | -------------------------------------------------------------------------------- /ui/src/app/core/directives/plugins.markdown.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, inject, OnInit } from '@angular/core' 2 | import { EmojiConvertor } from 'emoji-js' 3 | 4 | @Directive({ 5 | selector: 'markdown', 6 | standalone: true, 7 | }) 8 | export class PluginsMarkdownDirective implements OnInit { 9 | private el = inject(ElementRef) 10 | 11 | public ngOnInit() { 12 | // Ensure third party links open in a new window without a referrer 13 | const links = this.el.nativeElement.querySelectorAll('a') 14 | links.forEach((a: any) => { 15 | a.target = '_blank' 16 | a.rel = 'noopener noreferrer' 17 | }) 18 | 19 | // Replace colon emojis 20 | const emoji = new EmojiConvertor() 21 | this.el.nativeElement.innerHTML = emoji.replace_colons(this.el.nativeElement.innerHTML) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.component.scss: -------------------------------------------------------------------------------- 1 | .manage-plugin-config-external-icons { 2 | font-size: 1.5rem; 3 | font-weight: 700; 4 | line-height: 1; 5 | color: #000; 6 | opacity: 0.5; 7 | background-color: transparent; 8 | border: 0; 9 | } 10 | 11 | .loading-overlay { 12 | position: absolute; 13 | top: 16px; 14 | left: 16px; 15 | right: 16px; 16 | bottom: 16px; 17 | background-color: rgba(255, 255, 255, 0.7); 18 | } 19 | 20 | ::ng-deep body.dark-mode { 21 | .loading-overlay { 22 | background-color: rgba(36, 36, 36, 0.7); 23 | } 24 | } 25 | 26 | .custom-form-action-buttons { 27 | button:last-child { 28 | margin-right: 2px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { Bootstrap5FrameworkModule } from '@ng-formworks/bootstrap5' 3 | import { NgxMdModule } from 'ngx-md' 4 | 5 | import { CustomPluginsService } from '@/app/core/manage-plugins/custom-plugins/custom-plugins.service' 6 | 7 | @NgModule({ 8 | imports: [ 9 | Bootstrap5FrameworkModule, 10 | NgxMdModule, 11 | ], 12 | providers: [ 13 | CustomPluginsService, 14 | ], 15 | }) 16 | export class CustomPluginsModule {} 17 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/custom-plugins/homebridge-deconz/homebridge-deconz.component.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/custom-plugins/homebridge-deconz/homebridge-deconz.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { TranslateService } from '@ngx-translate/core' 3 | import { saveAs } from 'file-saver' 4 | import { ToastrService } from 'ngx-toastr' 5 | 6 | import { ApiService } from '@/app/core/api.service' 7 | 8 | @Component({ 9 | selector: 'app-homebridge-deconz', 10 | templateUrl: './homebridge-deconz.component.html', 11 | standalone: true, 12 | }) 13 | export class HomebridgeDeconzComponent { 14 | private $api = inject(ApiService) 15 | private $toastr = inject(ToastrService) 16 | private $translate = inject(TranslateService) 17 | 18 | public downloadDumpFile() { 19 | this.$api.get('/plugins/custom-plugins/homebridge-deconz/dump-file', { observe: 'response', responseType: 'blob' }).subscribe({ 20 | next: (res) => { 21 | saveAs(res.body, 'homebridge-deconz.json.gz') 22 | }, 23 | error: (error) => { 24 | console.error(error) 25 | this.$toastr.error(this.$translate.instant('plugins.settings.deconz.dump_no_exist'), this.$translate.instant('toast.title_error')) 26 | }, 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/custom-plugins/homebridge-hue/homebridge-hue.component.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/custom-plugins/homebridge-hue/homebridge-hue.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { TranslateService } from '@ngx-translate/core' 3 | import { saveAs } from 'file-saver' 4 | import { ToastrService } from 'ngx-toastr' 5 | 6 | import { ApiService } from '@/app/core/api.service' 7 | 8 | @Component({ 9 | selector: 'app-homebridge-hue', 10 | templateUrl: './homebridge-hue.component.html', 11 | standalone: true, 12 | }) 13 | export class HomebridgeHueComponent { 14 | private $api = inject(ApiService) 15 | private $translate = inject(TranslateService) 16 | private $toastr = inject(ToastrService) 17 | 18 | public downloadDumpFile() { 19 | this.$api.get('/plugins/custom-plugins/homebridge-hue/dump-file', { observe: 'response', responseType: 'blob' }).subscribe({ 20 | next: (res) => { 21 | saveAs(res.body, 'homebridge-hue.json.gz') 22 | }, 23 | error: (error) => { 24 | console.error(error) 25 | this.$toastr.error(this.$translate.instant('plugins.settings.hue.dump_no_exist'), this.$translate.instant('toast.title_error')) 26 | }, 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, Input } from '@angular/core' 2 | import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe, TranslateService } from '@ngx-translate/core' 4 | 5 | @Component({ 6 | templateUrl: './disable-plugin.component.html', 7 | standalone: true, 8 | imports: [ 9 | NgbAlert, 10 | TranslatePipe, 11 | ], 12 | }) 13 | export class DisablePluginComponent { 14 | private $activeModal = inject(NgbActiveModal) 15 | private $translate = inject(TranslateService) 16 | 17 | @Input() pluginName: string 18 | @Input() isConfigured = false 19 | @Input() isConfiguredDynamicPlatform = false 20 | @Input() keepOrphans = false 21 | 22 | public readonly keepOrphansName = `${this.$translate.instant('settings.startup.keep_accessories')}` 23 | public readonly keepOrphansValue = `${this.keepOrphans}` 24 | 25 | public dismissModal() { 26 | this.$activeModal.dismiss('Dismiss') 27 | } 28 | 29 | public closeModal() { 30 | this.$activeModal.close() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/donate/donate.component.scss: -------------------------------------------------------------------------------- 1 | .text-break-all { 2 | word-break: break-all; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/manage-plugin/manage-plugin.component.scss: -------------------------------------------------------------------------------- 1 | .plugin-modal-body { 2 | .release-notes { 3 | box-shadow: none; 4 | max-height: 39vh; 5 | overflow: auto; 6 | border: 0; 7 | border-top-left-radius: 0; 8 | border-top-right-radius: 0; 9 | } 10 | 11 | .release-tab { 12 | font-size: 1rem; 13 | color: lightgrey; 14 | } 15 | 16 | .tab-content { 17 | border-radius: 0 0 20px 20px; 18 | } 19 | } 20 | 21 | ::ng-deep .nav-tabs { 22 | --bs-nav-tabs-border-width: 0; 23 | } 24 | 25 | ::ng-deep .plugin-md { 26 | text-align: left; 27 | font-size: 0.85rem; 28 | word-wrap: break-word; 29 | 30 | img { 31 | max-width: 100%; 32 | } 33 | 34 | h1 { 35 | font-size: 1.3rem; 36 | font-weight: 300; 37 | } 38 | 39 | h2 { 40 | font-size: 1.2rem; 41 | font-weight: 300; 42 | } 43 | 44 | h3 { 45 | font-size: 1.1rem; 46 | font-weight: 300; 47 | } 48 | 49 | h4 { 50 | font-size: 1rem; 51 | font-weight: 300; 52 | } 53 | 54 | h5 { 55 | font-size: 0.9rem; 56 | font-weight: 300; 57 | } 58 | 59 | pre { 60 | padding: 16px; 61 | overflow: auto; 62 | font-size: 85%; 63 | line-height: 1.45; 64 | background-color: #f6f8fa; 65 | border-radius: 3px; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/manage-plugins.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { NgxMdModule } from 'ngx-md' 3 | 4 | import { CustomPluginsModule } from '@/app/core/manage-plugins/custom-plugins/custom-plugins.module' 5 | import { ManagePluginsService } from '@/app/core/manage-plugins/manage-plugins.service' 6 | 7 | @NgModule({ 8 | imports: [ 9 | NgxMdModule, 10 | CustomPluginsModule, 11 | ], 12 | providers: [ 13 | ManagePluginsService, 14 | ], 15 | }) 16 | export class ManagePluginsModule {} 17 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/manual-config/manual-config.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .no-padding-card-body { 3 | .card-body { 4 | padding: 0 0 0 0 !important; 5 | } 6 | } 7 | } 8 | 9 | ::ng-deep body.dark-mode { 10 | .no-padding-card-body { 11 | .card-body { 12 | background-color: #1e1e1e; 13 | } 14 | } 15 | } 16 | 17 | .manage-plugin-config-external-icons { 18 | font-size: 1.5rem; 19 | font-weight: 700; 20 | line-height: 1; 21 | color: #000; 22 | opacity: 0.5; 23 | background-color: transparent; 24 | border: 0; 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/plugin-bridge/plugin-bridge.component.scss: -------------------------------------------------------------------------------- 1 | .manage-plugin-config-external-icons { 2 | font-size: 1.5rem; 3 | font-weight: 700; 4 | line-height: 1; 5 | color: #000; 6 | opacity: 0.5; 7 | background-color: transparent; 8 | border: 0; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/plugin-config/plugin-config.component.scss: -------------------------------------------------------------------------------- 1 | .manage-plugin-config-external-icons { 2 | font-size: 1.5rem; 3 | font-weight: 700; 4 | line-height: 1; 5 | color: #000; 6 | opacity: 0.5; 7 | background-color: transparent; 8 | border: 0; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/core/manage-plugins/switch-to-scoped/switch-to-scoped.component.scss: -------------------------------------------------------------------------------- 1 | #plugin-output { 2 | height: 150px; 3 | overflow-y: auto; 4 | overflow-x: hidden; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/core/mobile-detect.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import MobileDetect from 'mobile-detect' 3 | 4 | function preventDefault(e: Event) { 5 | e.preventDefault() 6 | } 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class MobileDetectService { 12 | public detect: MobileDetect 13 | public isTouchMoveLocked = false 14 | 15 | constructor() { 16 | this.detect = new MobileDetect(window.navigator.userAgent) 17 | } 18 | 19 | public disableTouchMove() { 20 | if (!this.isTouchMoveLocked) { 21 | document.body.addEventListener('touchmove', preventDefault, { passive: false }) 22 | this.isTouchMoveLocked = true 23 | } 24 | } 25 | 26 | public enableTouchMove() { 27 | document.body.removeEventListener('touchmove', preventDefault) 28 | this.isTouchMoveLocked = false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/core/monaco-editor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { Subject } from 'rxjs' 3 | 4 | const readyEvent = new Subject() 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class MonacoEditorService { 10 | public readyEvent: Subject 11 | 12 | constructor() { 13 | this.readyEvent = readyEvent 14 | } 15 | } 16 | 17 | export function onMonacoLoad() { 18 | readyEvent.next(undefined) 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { Subject } from 'rxjs' 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class NotificationService { 8 | readonly raspberryPiThrottled: Subject> = new Subject() 9 | readonly formAuthEnabled: Subject = new Subject() 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/core/pipes/convert-mired.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | @Pipe({ 4 | name: 'convertMired', 5 | standalone: true, 6 | }) 7 | export class ConvertMiredPipe implements PipeTransform { 8 | transform(mired: boolean | string | number): boolean | string | number { 9 | if (typeof mired !== 'number') { 10 | return mired 11 | } 12 | // Input a mired value and convert it to kelvin 13 | // Return a string like `500M | 2000K` 14 | const kelvin = 1000000 / mired 15 | const miredValue = Math.round(mired) 16 | const kelvinValue = Math.round(kelvin) 17 | return `${miredValue}M (${kelvinValue}K)` 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/pipes/convert-temp.pipe.ts: -------------------------------------------------------------------------------- 1 | import { inject, Pipe, PipeTransform } from '@angular/core' 2 | 3 | import { SettingsService } from '@/app/core/settings.service' 4 | 5 | @Pipe({ 6 | name: 'convertTemp', 7 | standalone: true, 8 | }) 9 | export class ConvertTempPipe implements PipeTransform { 10 | private $settings = inject(SettingsService) 11 | 12 | transform(value: number, unit: 'c' | 'f' = this.$settings.env.temperatureUnits): number { 13 | if (unit === 'f') { 14 | return Math.round((value * 1.8 + 32) * 10) / 10 15 | } 16 | return Math.round(value * 10) / 10 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/core/pipes/duration.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | @Pipe({ 4 | name: 'duration', 5 | standalone: true, 6 | }) 7 | export class DurationPipe implements PipeTransform { 8 | transform(value: number): string { 9 | if (typeof value !== 'number' || Number.isNaN(value)) { 10 | return '' 11 | } 12 | const minutes = Math.floor(value / 60) 13 | const seconds = value % 60 14 | return [ 15 | minutes > 0 ? `${minutes.toString()}m` : '', 16 | seconds > 0 ? `${seconds.toString()}s` : '', 17 | ].filter(Boolean).join(' ') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/pipes/interpolate-md.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | @Pipe({ 4 | name: 'interpolateMd', 5 | standalone: true, 6 | }) 7 | export class InterpolateMdPipe implements PipeTransform { 8 | transform(value: string): string { 9 | return value.replace(/\$\{\{HOSTNAME\}\}/g, location.hostname) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/app/core/pipes/prettify.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | @Pipe({ 4 | name: 'prettify', 5 | standalone: true, 6 | }) 7 | export class PrettifyPipe implements PipeTransform { 8 | transform(value: string): string { 9 | if (typeof value !== 'string') { 10 | return value 11 | } 12 | 13 | return value 14 | .replace(/_/g, ' ') // Replace underscores with spaces 15 | .toLowerCase() 16 | .replace(/\b\w/g, char => char.toUpperCase()) // Capitalize the first letter of each word 17 | .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space before uppercase letters that follow lowercase letters 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/core/pipes/service-to-translation-string.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | @Pipe({ 4 | name: 'serviceToTranslationString', 5 | standalone: true, 6 | }) 7 | export class ServiceToTranslationStringPipe implements PipeTransform { 8 | transform(value: string): string { 9 | if (typeof value !== 'string' || !value) { 10 | return value 11 | } 12 | // Replace capital letters (except the first) with _ + lowercase 13 | const service = value 14 | .replace(/^([A-Z])/, match => match.toLowerCase()) 15 | .replace(/([A-Z])/g, match => `_${match.toLowerCase()}`) 16 | return `accessories.core.${service}` 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/accessories-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadComponent: () => import('@/app/modules/accessories/accessories.component').then(m => m.AccessoriesComponent), 8 | }, 9 | ] 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | }) 15 | export class AccessoriesRoutingModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/accessories.component.scss: -------------------------------------------------------------------------------- 1 | .room-title { 2 | margin-left: 10px; 3 | @media (max-width: 575px) { 4 | margin-left: 7px; 5 | } 6 | } 7 | 8 | .services-bag { 9 | min-height: 170px; 10 | @media (max-width: 575px) { 11 | min-height: initial; 12 | } 13 | } 14 | 15 | .cursor-move { 16 | cursor: move; 17 | } 18 | 19 | a:hover { 20 | text-decoration: none !important; 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/accessories.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { AccessoriesCoreModule } from '@/app/core/accessories/accessories.module' 4 | import { AccessoriesRoutingModule } from '@/app/modules/accessories/accessories-routing.module' 5 | 6 | @NgModule({ 7 | imports: [ 8 | AccessoriesCoreModule, 9 | AccessoriesRoutingModule, 10 | ], 11 | }) 12 | export class AccessoriesModule {} 13 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/accessory-support/accessory-support.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { SupportBannerComponent } from '@/app/core/components/support-banner/support-banner.component' 6 | 7 | @Component({ 8 | templateUrl: './accessory-support.component.html', 9 | standalone: true, 10 | imports: [TranslatePipe, SupportBannerComponent], 11 | }) 12 | export class AccessorySupportComponent { 13 | private $activeModal = inject(NgbActiveModal) 14 | 15 | public dismissModal() { 16 | this.$activeModal.dismiss('Dismiss') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/add-room/add-room.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, Input } from '@angular/core' 2 | import { FormsModule } from '@angular/forms' 3 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 4 | import { TranslatePipe } from '@ngx-translate/core' 5 | 6 | @Component({ 7 | templateUrl: './add-room.component.html', 8 | standalone: true, 9 | imports: [FormsModule, TranslatePipe], 10 | }) 11 | export class AddRoomComponent { 12 | private $activeModal = inject(NgbActiveModal) 13 | 14 | @Input() public roomName: string 15 | 16 | public dismissModal() { 17 | this.$activeModal.dismiss('Dismiss') 18 | } 19 | 20 | public closeModal(roomName: string) { 21 | this.$activeModal.close(roomName) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/drag-here-placeholder/drag-here-placeholder.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
{{ 'accessories.control.drag_here' | translate }}
4 |
5 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/drag-here-placeholder/drag-here-placeholder.component.scss: -------------------------------------------------------------------------------- 1 | .accessory-box { 2 | opacity: 0.2; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/app/modules/accessories/drag-here-placeholder/drag-here-placeholder.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { TranslatePipe } from '@ngx-translate/core' 3 | 4 | @Component({ 5 | selector: 'app-drag-here-placeholder', 6 | templateUrl: './drag-here-placeholder.component.html', 7 | styleUrls: ['./drag-here-placeholder.component.scss'], 8 | standalone: true, 9 | imports: [TranslatePipe], 10 | }) 11 | export class DragHerePlaceholderComponent {} 12 | -------------------------------------------------------------------------------- /ui/src/app/modules/config-editor/config-editor-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { ConfigEditorResolver } from '@/app/modules/config-editor/config-editor.resolver' 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | loadComponent: () => import('@/app/modules/config-editor/config-editor.component').then(m => m.ConfigEditorComponent), 10 | resolve: { 11 | config: ConfigEditorResolver, 12 | }, 13 | }, 14 | ] 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule], 19 | }) 20 | export class ConfigEditorRoutingModule {} 21 | -------------------------------------------------------------------------------- /ui/src/app/modules/config-editor/config-editor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { ConfigEditorRoutingModule } from '@/app/modules/config-editor/config-editor-routing.module' 4 | import { ConfigEditorResolver } from '@/app/modules/config-editor/config-editor.resolver' 5 | 6 | @NgModule({ 7 | imports: [ 8 | ConfigEditorRoutingModule, 9 | ], 10 | providers: [ 11 | ConfigEditorResolver, 12 | ], 13 | }) 14 | export class ConfigEditorModule {} 15 | -------------------------------------------------------------------------------- /ui/src/app/modules/config-editor/config-editor.resolver.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core' 2 | import { Resolve, Router } from '@angular/router' 3 | import { TranslateService } from '@ngx-translate/core' 4 | import { ToastrService } from 'ngx-toastr' 5 | import { firstValueFrom } from 'rxjs' 6 | 7 | import { ApiService } from '@/app/core/api.service' 8 | 9 | @Injectable() 10 | export class ConfigEditorResolver implements Resolve { 11 | private $api = inject(ApiService) 12 | private $router = inject(Router) 13 | private $toastr = inject(ToastrService) 14 | private $translate = inject(TranslateService) 15 | 16 | public async resolve() { 17 | try { 18 | const json = await firstValueFrom(this.$api.get('/config-editor')) 19 | return JSON.stringify(json, null, 4) 20 | } catch (error) { 21 | console.error(error) 22 | this.$toastr.error(error.message, this.$translate.instant('toast.title_error')) 23 | void this.$router.navigate(['/']) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/modules/login/login.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep body { 2 | overflow: scroll !important; 3 | } 4 | 5 | ::ng-deep body.dark-mode { 6 | .login-card { 7 | background-color: #2b2b2b; 8 | color: #ffffff; 9 | } 10 | } 11 | 12 | .login-container { 13 | z-index: -1; 14 | padding-top: 2em; 15 | padding-bottom: 2em; 16 | min-height: 100%; 17 | background-size: 300% 300%; 18 | } 19 | 20 | .anim { 21 | -webkit-animation: gradient 20s ease infinite; 22 | -moz-animation: gradient 20s ease infinite; 23 | -o-animation: gradient 20s ease infinite; 24 | animation: gradient 20s ease infinite; 25 | } 26 | 27 | @keyframes gradient { 28 | 0% { 29 | background-position: 0 50%; 30 | } 31 | 32 | 50% { 33 | background-position: 100% 50%; 34 | } 35 | 36 | 100% { 37 | background-position: 0 50%; 38 | } 39 | } 40 | 41 | .login-card { 42 | max-width: 550px; 43 | border-radius: 1rem; 44 | padding-right: 25px; 45 | padding-left: 25px; 46 | background-color: rgba(255, 255, 255, 0.9); 47 | 48 | .form-control:focus { 49 | background-color: inherit !important; 50 | } 51 | 52 | @media screen and (max-width: 575px) { 53 | margin-left: 1em; 54 | margin-right: 1em; 55 | } 56 | } 57 | 58 | .homebridge-logo { 59 | margin-bottom: 10px; 60 | } 61 | -------------------------------------------------------------------------------- /ui/src/app/modules/login/login.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core' 2 | import { CanActivate, Router } from '@angular/router' 3 | import { firstValueFrom } from 'rxjs' 4 | 5 | import { AuthService } from '@/app/core/auth/auth.service' 6 | import { SettingsService } from '@/app/core/settings.service' 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class LoginGuard implements CanActivate { 12 | private $auth = inject(AuthService) 13 | private $router = inject(Router) 14 | private $settings = inject(SettingsService) 15 | 16 | public async canActivate(): Promise { 17 | // Ensure app settings are loaded 18 | if (!this.$settings.settingsLoaded) { 19 | await firstValueFrom(this.$settings.onSettingsLoaded) 20 | } 21 | 22 | if (this.$settings.env.setupWizardComplete === false) { 23 | // Redirect to set up wizard page 24 | void this.$router.navigate(['/setup']) 25 | return false 26 | } 27 | 28 | // If using not using auth, or already logged in, redirect back to home screen 29 | if (this.$settings.formAuth === false || this.$auth.isLoggedIn()) { 30 | // Redirect to login page 31 | void this.$router.navigate(['/']) 32 | return false 33 | } 34 | 35 | return true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/app/modules/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { LoginGuard } from '@/app/modules/login/login.guard' 4 | 5 | @NgModule({ 6 | providers: [ 7 | LoginGuard, 8 | ], 9 | }) 10 | export class LoginModule {} 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/logs/logs-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadComponent: () => import('@/app/modules/logs/logs.component').then(m => m.LogsComponent), 8 | }, 9 | ] 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | }) 15 | export class LogsRoutingModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/logs/logs.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ 'menu.linux.label_logs' | translate }}

4 |
5 | @if (isAdmin) { 6 |
7 | 19 | 31 |
32 | } 33 |
34 | 35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /ui/src/app/modules/logs/logs.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { LogsRoutingModule } from '@/app/modules/logs/logs-routing.module' 4 | 5 | @NgModule({ 6 | imports: [ 7 | LogsRoutingModule, 8 | ], 9 | }) 10 | export class LogsModule {} 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/docker/container-restart/container-restart.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ 'menu.restart.title' | translate }}

3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |

{{ 'platform.docker.title_restarting' | translate }}

11 | @if (error) { 12 |
{{ error }}
13 | } @else { 14 |

{{ 'restart.please_wait_while_server_restarts' | translate }}

15 |
16 |
17 | 18 |
19 |
20 | } @if (timeout) { 21 |
22 |

{{ 'platform.docker.server_long_time' | translate }}

23 |

24 |
25 | } 26 |
27 |
28 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/docker/container-restart/container-restart.component.scss: -------------------------------------------------------------------------------- 1 | .restart-progress-box { 2 | font-size: 22px; 3 | font-weight: 300; 4 | margin-top: 15px; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/docker/docker-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { StartupScriptResolver } from '@/app/modules/platform-tools/docker/startup-script/startup-script.resolver' 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | redirectTo: '/', 10 | pathMatch: 'full', 11 | }, 12 | { 13 | path: 'startup-script', 14 | loadComponent: () => import('@/app/modules/platform-tools/docker/startup-script/startup-script.component').then(m => m.StartupScriptComponent), 15 | resolve: { 16 | startupScript: StartupScriptResolver, 17 | }, 18 | }, 19 | { 20 | path: 'restart-container', 21 | loadComponent: () => import('@/app/modules/platform-tools/docker/container-restart/container-restart.component').then(m => m.ContainerRestartComponent), 22 | }, 23 | ] 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forChild(routes)], 27 | exports: [RouterModule], 28 | }) 29 | export class DockerRoutingModule {} 30 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/docker/docker.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { DockerRoutingModule } from '@/app/modules/platform-tools/docker/docker-routing.module' 4 | import { StartupScriptResolver } from '@/app/modules/platform-tools/docker/startup-script/startup-script.resolver' 5 | 6 | @NgModule({ 7 | imports: [ 8 | DockerRoutingModule, 9 | ], 10 | providers: [ 11 | StartupScriptResolver, 12 | ], 13 | }) 14 | export class DockerModule {} 15 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/docker/startup-script/startup-script.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

startup.sh

5 |
6 |
7 | 14 |
15 |
16 | @if (!isMobile) { 17 | 25 | 26 | } @if (isMobile) { 27 | 37 | } 38 |
39 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/docker/startup-script/startup-script.resolver.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core' 2 | import { Resolve, Router } from '@angular/router' 3 | import { TranslateService } from '@ngx-translate/core' 4 | import { ToastrService } from 'ngx-toastr' 5 | import { firstValueFrom } from 'rxjs' 6 | 7 | import { ApiService } from '@/app/core/api.service' 8 | 9 | @Injectable() 10 | export class StartupScriptResolver implements Resolve { 11 | private $api = inject(ApiService) 12 | private $router = inject(Router) 13 | private $toastr = inject(ToastrService) 14 | private $translate = inject(TranslateService) 15 | 16 | public async resolve() { 17 | try { 18 | return await firstValueFrom(this.$api.get('/platform-tools/docker/startup-script')) 19 | } catch (error) { 20 | console.error(error) 21 | this.$toastr.error(error.message, this.$translate.instant('toast.title_error')) 22 | void this.$router.navigate(['/']) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/linux/linux-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | redirectTo: '/', 8 | pathMatch: 'full', 9 | }, 10 | { 11 | path: 'restart-server', 12 | loadComponent: () => import('@/app/modules/platform-tools/linux/restart-linux/restart-linux.component').then(m => m.RestartLinuxComponent), 13 | }, 14 | { 15 | path: 'shutdown-server', 16 | loadComponent: () => import('@/app/modules/platform-tools/linux/shutdown-linux/shutdown-linux.component').then(m => m.ShutdownLinuxComponent), 17 | }, 18 | ] 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forChild(routes)], 22 | exports: [RouterModule], 23 | }) 24 | export class LinuxRoutingModule {} 25 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/linux/linux.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { LinuxRoutingModule } from '@/app/modules/platform-tools/linux/linux-routing.module' 4 | 5 | @NgModule({ 6 | imports: [ 7 | LinuxRoutingModule, 8 | ], 9 | }) 10 | export class LinuxModule {} 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/linux/restart-linux/restart-linux.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ 'menu.restart.title' | translate }}

3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |

{{ 'platform.linux.restarting_server' | translate }}

11 | @if (error) { 12 |
{{ error }}
13 | } @else { 14 |

{{ 'restart.please_wait_while_server_restarts' | translate }}

15 |
16 |
17 | 18 |
19 |
20 | } @if (timeout) { 21 |
{{ 'platform.linux.long_time' | translate }}
22 | } 23 |
24 |
25 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/linux/restart-linux/restart-linux.component.scss: -------------------------------------------------------------------------------- 1 | .restart-progress-box { 2 | font-size: 22px; 3 | font-weight: 300; 4 | margin-top: 15px; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/linux/shutdown-linux/shutdown-linux.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ 'menu.restart.title' | translate }}

3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |

{{ 'platform.linux.shutting_down_server' | translate }}

11 | @if (error) { 12 |
{{ error }}
13 | } @else { 14 |

{{ 'platform.linux.server_will_power_down' | translate }}

15 | } 16 |
17 |
18 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/linux/shutdown-linux/shutdown-linux.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, OnInit } from '@angular/core' 2 | import { TranslatePipe, TranslateService } from '@ngx-translate/core' 3 | import { ToastrService } from 'ngx-toastr' 4 | 5 | import { ApiService } from '@/app/core/api.service' 6 | 7 | @Component({ 8 | templateUrl: './shutdown-linux.component.html', 9 | standalone: true, 10 | imports: [TranslatePipe], 11 | }) 12 | export class ShutdownLinuxComponent implements OnInit { 13 | private $api = inject(ApiService) 14 | private $toastr = inject(ToastrService) 15 | private $translate = inject(TranslateService) 16 | 17 | public error: any = false 18 | 19 | public ngOnInit() { 20 | this.$api.put('/platform-tools/linux/shutdown-host', {}).subscribe({ 21 | error: (error) => { 22 | console.error(error) 23 | this.error = this.$translate.instant('platform.linux.server_restart_error') 24 | this.$toastr.error(this.$translate.instant('platform.linux.server_restart_error'), this.$translate.instant('toast.title_error')) 25 | }, 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/platform-tools-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | redirectTo: '/', 8 | pathMatch: 'full', 9 | }, 10 | { 11 | path: 'docker', 12 | loadChildren: () => import('./docker/docker.module').then(m => m.DockerModule), 13 | }, 14 | { 15 | path: 'linux', 16 | loadChildren: () => import('./linux/linux.module').then(m => m.LinuxModule), 17 | }, 18 | { 19 | path: 'terminal', 20 | loadChildren: () => import('./terminal/terminal.module').then(m => m.TerminalModule), 21 | }, 22 | ] 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forChild(routes)], 26 | exports: [RouterModule], 27 | }) 28 | export class PlatformToolsRoutingModule {} 29 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/platform-tools.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { PlatformToolsRoutingModule } from '@/app/modules/platform-tools/platform-tools-routing.module' 4 | 5 | @NgModule({ 6 | declarations: [], 7 | imports: [ 8 | PlatformToolsRoutingModule, 9 | ], 10 | }) 11 | export class PlatformToolsModule {} 12 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/terminal/terminal-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadComponent: () => import('@/app/modules/platform-tools/terminal/terminal.component').then(m => m.TerminalComponent), 8 | canDeactivate: [(component: any) => component.canDeactivate ? component.canDeactivate() : true], 9 | }, 10 | ] 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | }) 16 | export class TerminalRoutingModule {} 17 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/terminal/terminal.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ 'menu.linux.label_terminal' | translate }}

4 |
5 |
6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /ui/src/app/modules/platform-tools/terminal/terminal.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { TerminalRoutingModule } from '@/app/modules/platform-tools/terminal/terminal-routing.module' 4 | 5 | @NgModule({ 6 | imports: [ 7 | TerminalRoutingModule, 8 | ], 9 | }) 10 | export class TerminalModule {} 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/plugins/plugin-card/plugin-info/plugin-info.component.scss: -------------------------------------------------------------------------------- 1 | .plugin-icon { 2 | height: 75px; 3 | width: 75px; 4 | border-radius: 15px; 5 | border: 1px solid #222222; 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/modules/plugins/plugin-support/plugin-support.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { SupportBannerComponent } from '@/app/core/components/support-banner/support-banner.component' 6 | 7 | @Component({ 8 | templateUrl: './plugin-support.component.html', 9 | standalone: true, 10 | imports: [TranslatePipe, SupportBannerComponent], 11 | }) 12 | export class PluginSupportComponent { 13 | private $activeModal = inject(NgbActiveModal) 14 | 15 | public dismissModal() { 16 | this.$activeModal.dismiss('Dismiss') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/modules/plugins/plugins-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadComponent: () => import('@/app/modules/plugins/plugins.component').then(m => m.PluginsComponent), 8 | }, 9 | ] 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | }) 15 | export class PluginsRoutingModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/plugins/plugins.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { ManagePluginsModule } from '@/app/core/manage-plugins/manage-plugins.module' 4 | import { PluginsRoutingModule } from '@/app/modules/plugins/plugins-routing.module' 5 | 6 | @NgModule({ 7 | imports: [ 8 | ManagePluginsModule, 9 | PluginsRoutingModule, 10 | ], 11 | }) 12 | export class PluginsModule {} 13 | -------------------------------------------------------------------------------- /ui/src/app/modules/power-options/power-options-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadComponent: () => import('@/app/modules/power-options/power-options.component').then(m => m.PowerOptionsComponent), 8 | }, 9 | ] 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | }) 15 | export class PowerOptionsRoutingModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/power-options/power-options.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { PowerOptionsRoutingModule } from '@/app/modules/power-options/power-options-routing.module' 4 | 5 | @NgModule({ 6 | imports: [ 7 | PowerOptionsRoutingModule, 8 | ], 9 | }) 10 | export class PowerOptionsModule {} 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/restart/restart.component.scss: -------------------------------------------------------------------------------- 1 | .restart-progress-box { 2 | font-size: 22px; 3 | font-weight: 300; 4 | margin-top: 15px; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/modules/restart/restart.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | @NgModule({}) 4 | export class RestartModule {} 5 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/backup/backup.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core' 2 | import { TranslateService } from '@ngx-translate/core' 3 | import { saveAs } from 'file-saver' 4 | import { ToastrService } from 'ngx-toastr' 5 | import { firstValueFrom } from 'rxjs' 6 | 7 | import { ApiService } from '@/app/core/api.service' 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class BackupService { 13 | private $api = inject(ApiService) 14 | private $toastr = inject(ToastrService) 15 | private $translate = inject(TranslateService) 16 | 17 | public async downloadBackup(): Promise { 18 | const res = await firstValueFrom(this.$api.get('/backup/download', { 19 | observe: 'response', 20 | responseType: 'blob', 21 | })) 22 | const archiveName = res.headers.get('File-Name') || 'homebridge-backup.tar.gz' 23 | const sizeInBytes = res.body.size 24 | if (sizeInBytes > globalThis.backup.maxBackupSize) { 25 | const message = this.$translate.instant('backup.backup_exceeds_max_size', { 26 | maxBackupSizeText: globalThis.backup.maxBackupSizeText, 27 | size: `${(sizeInBytes / (1024 * 1024)).toFixed(1)}MB`, 28 | }) 29 | this.$toastr.warning(message, this.$translate.instant('toast.title_warning')) 30 | } 31 | saveAs(res.body, archiveName) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/settings-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { AdminGuard } from '@/app/core/auth/admin.guard' 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | loadComponent: () => import('@/app/modules/settings/settings.component').then(m => m.SettingsComponent), 10 | canActivate: [AdminGuard], 11 | }, 12 | ] 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forChild(routes)], 16 | exports: [RouterModule], 17 | }) 18 | export class SettingsRoutingModule {} 19 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/settings-support/settings-support.component.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/settings-support/settings-support.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { SupportBannerComponent } from '@/app/core/components/support-banner/support-banner.component' 6 | 7 | @Component({ 8 | templateUrl: './settings-support.component.html', 9 | standalone: true, 10 | imports: [TranslatePipe, SupportBannerComponent], 11 | }) 12 | export class SettingsSupportComponent { 13 | private $activeModal = inject(NgbActiveModal) 14 | 15 | public dismissModal() { 16 | this.$activeModal.dismiss('Dismiss') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | outline: none; 3 | box-sizing: inherit !important; 4 | background-color: inherit; 5 | height: 50px !important; 6 | width: 100%; 7 | border-width: 0.5px; 8 | padding: 10px; 9 | margin-bottom: 15px; 10 | 11 | @media (hover: hover) { 12 | &:hover { 13 | border: 1px solid #000000; 14 | } 15 | } 16 | &:focus { 17 | border: 1px solid #000000; 18 | box-shadow: 0 1px 0 0 #000000; 19 | } 20 | } 21 | 22 | .search-bar-clear { 23 | position: absolute; 24 | right: 30px; 25 | font-size: 25px; 26 | color: #d0d0d0; 27 | line-height: 50px; 28 | cursor: pointer; 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/settings.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface NetworkAdapterAvailable { 2 | carrierChanges?: number 3 | default?: boolean 4 | dhcp?: boolean 5 | dnsSuffix?: string 6 | duplex?: string 7 | ieee8021xAuth?: string 8 | ieee8021xState?: string 9 | iface: string 10 | ifaceName: string 11 | internal: boolean 12 | ip4: string 13 | ip4subnet: string 14 | ip6: string 15 | ip6subnet: string 16 | mac: string 17 | mtu: number 18 | missing?: boolean 19 | operstate: string 20 | selected: boolean 21 | speed: number 22 | type: string 23 | virtual?: boolean 24 | } 25 | 26 | export interface NetworkAdapterSelected { 27 | iface: string 28 | ip4?: string 29 | ip6?: string 30 | missing: boolean 31 | selected: boolean 32 | } 33 | 34 | export interface Pairing { 35 | _id: string 36 | _username: string 37 | _main?: boolean 38 | name: string 39 | accessories: any[] 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SettingsRoutingModule } from '@/app/modules/settings/settings-routing.module' 4 | 5 | @NgModule({ 6 | imports: [ 7 | SettingsRoutingModule, 8 | ], 9 | }) 10 | export class SettingsModule {} 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/settings/wallpaper/wallpaper.component.scss: -------------------------------------------------------------------------------- 1 | .anim { 2 | -webkit-animation: gradient 20s ease infinite; 3 | -moz-animation: gradient 20s ease infinite; 4 | -o-animation: gradient 20s ease infinite; 5 | animation: gradient 20s ease infinite; 6 | } 7 | 8 | @keyframes gradient { 9 | 0% { 10 | background-position: 0 50%; 11 | } 12 | 13 | 50% { 14 | background-position: 100% 50%; 15 | } 16 | 17 | 100% { 18 | background-position: 0 50%; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/modules/setup-wizard/setup-wizard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadComponent: () => import('@/app/modules/setup-wizard/setup-wizard.component').then(m => m.SetupWizardComponent), 8 | }, 9 | ] 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | }) 15 | export class SetupWizardRoutingModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/setup-wizard/setup-wizard.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core' 2 | import { CanActivate, Router } from '@angular/router' 3 | import { firstValueFrom } from 'rxjs' 4 | 5 | import { SettingsService } from '@/app/core/settings.service' 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class SetupWizardGuard implements CanActivate { 11 | private $router = inject(Router) 12 | private $settings = inject(SettingsService) 13 | 14 | public async canActivate(): Promise { 15 | if (!this.$settings.settingsLoaded) { 16 | await firstValueFrom(this.$settings.onSettingsLoaded) 17 | } 18 | 19 | if (this.$settings.env.setupWizardComplete === false) { 20 | return true 21 | } 22 | 23 | void this.$router.navigate(['/']) 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/modules/setup-wizard/setup-wizard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SetupWizardRoutingModule } from '@/app/modules/setup-wizard/setup-wizard-routing.module' 4 | import { SetupWizardGuard } from '@/app/modules/setup-wizard/setup-wizard.guard' 5 | 6 | @NgModule({ 7 | imports: [ 8 | SetupWizardRoutingModule, 9 | ], 10 | providers: [ 11 | SetupWizardGuard, 12 | ], 13 | }) 14 | export class SetupWizardModule {} 15 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/credits/credits.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | @Component({ 6 | templateUrl: './credits.component.html', 7 | standalone: true, 8 | imports: [TranslatePipe], 9 | }) 10 | export class CreditsComponent { 11 | private $activeModal = inject(NgbActiveModal) 12 | 13 | public dismissModal() { 14 | this.$activeModal.dismiss('Dismiss') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/status.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { provideCharts, withDefaultRegisterables } from 'ng2-charts' 3 | 4 | import { AccessoriesCoreModule } from '@/app/core/accessories/accessories.module' 5 | import { ManagePluginsModule } from '@/app/core/manage-plugins/manage-plugins.module' 6 | 7 | @NgModule({ 8 | imports: [ 9 | AccessoriesCoreModule, 10 | ManagePluginsModule, 11 | ], 12 | providers: [ 13 | provideCharts(withDefaultRegisterables()), 14 | ], 15 | }) 16 | export class StatusModule {} 17 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/accessories-widget/accessories-widget.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ 'menu.label_accessories' | translate }} 4 |
5 | @if (dashboardAccessories.length) { 6 |
11 | @for (service of dashboardAccessories; track service) { @if (!service.hidden) { 12 |
13 | 14 |
15 | } } 16 |
17 | } @if (loaded && !dashboardAccessories.length) { 18 |
19 |
20 |

21 |

{{ 'status.widget.accessories.choose_accessories' | translate }}

22 |
23 |
24 | } 25 |
26 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/bridges-widget/bridges-widget.component.scss: -------------------------------------------------------------------------------- 1 | .flex-child { 2 | white-space: nowrap; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | } 6 | 7 | .hb-status-icon { 8 | font-size: 20px; 9 | } 10 | 11 | .hb-status-item { 12 | @media (max-width: 767px) { 13 | width: 100%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/clock-widget/clock-widget.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
{{ currentTime | date:widget.timeFormat }}
6 |
{{ currentTime | date:widget.dateFormat }}
7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/clock-widget/clock-widget.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common' 2 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' 3 | import { interval, Subscription } from 'rxjs' 4 | 5 | import { Widget } from '@/app/modules/status/widgets/widgets.interfaces' 6 | 7 | @Component({ 8 | templateUrl: './clock-widget.component.html', 9 | standalone: true, 10 | imports: [DatePipe], 11 | }) 12 | export class ClockWidgetComponent implements OnInit, OnDestroy { 13 | private secondsCounter = interval(1000) 14 | private secondsCounterSubscription: Subscription 15 | 16 | @Input() widget: Widget 17 | 18 | public currentTime: Date = new Date() 19 | 20 | public ngOnInit() { 21 | if (!this.widget.timeFormat) { 22 | this.widget.timeFormat = 'H:mm' 23 | } 24 | if (!this.widget.dateFormat) { 25 | this.widget.dateFormat = 'yyyy-MM-dd' 26 | } 27 | 28 | this.secondsCounterSubscription = this.secondsCounter.subscribe(() => { 29 | this.currentTime = new Date() 30 | }) 31 | } 32 | 33 | public ngOnDestroy() { 34 | this.secondsCounterSubscription.unsubscribe() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/cpu-widget/cpu-widget.component.scss: -------------------------------------------------------------------------------- 1 | .widget-chart { 2 | position: absolute; 3 | z-index: -1; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/hap-qrcode-widget/hap-qrcode-widget.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | @if (setupUri) { 6 |
7 | 8 |
9 | } 10 |
11 |
12 |
13 |

{{ pin }}

14 |

15 | 22 | {{ (paired ? 'status.widget.qr_paired' : 'status.widget.qr_unpaired') | translate }} @if (!paired) { 23 | · {{ 'status.code_scan' | translate }} 24 | } 25 |

26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/homebridge-logs-widget/homebridge-logs-widget.component.html: -------------------------------------------------------------------------------- 1 |
6 |
15 | {{ 'status.widget.homebridge_logs' | translate }} 16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/memory-widget/memory-widget.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ 'status.memory.title_memory' | translate }} 4 |
5 | 13 |
14 |
15 |
16 |
17 |
{{ totalMemory | number:'1.0-2' }} GB
18 |
{{ 'status.memory.label_total' | translate }}
19 |
20 |
21 |
{{ freeMemory | number:'1.0-2' }} GB
22 |
{{ 'status.memory.label_available' | translate }}
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/memory-widget/memory-widget.component.scss: -------------------------------------------------------------------------------- 1 | .widget-chart { 2 | position: absolute; 3 | z-index: -1; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/network-widget/network-widget.component.scss: -------------------------------------------------------------------------------- 1 | .widget-chart { 2 | position: absolute; 3 | z-index: -1; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/system-info-widget/system-info-widget.component.scss: -------------------------------------------------------------------------------- 1 | table.table-sm th, 2 | table.table-sm td { 3 | padding-top: 0.3rem; 4 | padding-bottom: 0.3rem; 5 | } 6 | 7 | .system-info-link { 8 | @media (hover: hover) { 9 | &:hover { 10 | text-decoration: underline; 11 | } 12 | } 13 | } 14 | 15 | ::ng-deep body.dark-mode { 16 | .system-info-link { 17 | color: #ffffff !important; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/terminal-widget/terminal-widget.component.html: -------------------------------------------------------------------------------- 1 |
6 |
15 | Homebridge {{ 'menu.docker.terminal' | translate }} 16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/update-info-widget/update-info-widget.component.scss: -------------------------------------------------------------------------------- 1 | .hb-status-icon { 2 | font-size: 20px; 3 | } 4 | 5 | .hb-status-item { 6 | @media (max-width: 767px) { 7 | width: 100%; 8 | } 9 | } 10 | 11 | ::ng-deep body.dark-mode { 12 | .card-link-title { 13 | color: #ffffff !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/status/widgets/uptime-widget/uptime-widget.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ 'status.uptime.title_uptime' | translate }} 4 |
5 |
6 |
7 |
8 |
{{ serverUptime }}
9 |
{{ 'status.widget.uptime.label_server' | translate }}
10 |
11 |
12 |
{{ processUptime }}
13 |
{{ 'status.widget.uptime.label_process' | translate }}
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /ui/src/app/modules/support/support-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadComponent: () => import('@/app/modules/support/support.component').then(m => m.SupportComponent), 8 | }, 9 | ] 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | }) 15 | export class SupportRoutingModule {} 16 | -------------------------------------------------------------------------------- /ui/src/app/modules/support/support.component.ts: -------------------------------------------------------------------------------- 1 | import { NgClass } from '@angular/common' 2 | import { Component, inject, OnInit } from '@angular/core' 3 | import { TranslatePipe, TranslateService } from '@ngx-translate/core' 4 | 5 | import { SettingsService } from '@/app/core/settings.service' 6 | import { environment } from '@/environments/environment' 7 | 8 | @Component({ 9 | templateUrl: './support.component.html', 10 | standalone: true, 11 | imports: [ 12 | NgClass, 13 | TranslatePipe, 14 | ], 15 | }) 16 | export class SupportComponent implements OnInit { 17 | private $settings = inject(SettingsService) 18 | private $translate = inject(TranslateService) 19 | private swaggerEndpoint = '/swagger' 20 | public showFields = { 21 | general: true, 22 | dev: true, 23 | } 24 | 25 | public ngOnInit() { 26 | // Set page title 27 | const title = this.$translate.instant('support.title') 28 | this.$settings.setPageTitle(title) 29 | } 30 | 31 | public get swaggerUrl(): string { 32 | // In development mode, point to the backend server directly 33 | return environment.production 34 | ? this.swaggerEndpoint 35 | : `${environment.api.origin}${this.swaggerEndpoint}` 36 | } 37 | 38 | public toggleSection(section: string) { 39 | this.showFields[section] = !this.showFields[section] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/app/modules/support/support.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SupportRoutingModule } from '@/app/modules/support/support-routing.module' 4 | 5 | @NgModule({ 6 | imports: [ 7 | SupportRoutingModule, 8 | ], 9 | }) 10 | export class SupportModule {} 11 | -------------------------------------------------------------------------------- /ui/src/app/modules/users/users-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { UsersResolver } from '@/app/modules/users/users.resolver' 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | loadComponent: () => import('@/app/modules/users/users.component').then(m => m.UsersComponent), 10 | resolve: { 11 | homebridgeUsers: UsersResolver, 12 | }, 13 | }, 14 | ] 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule], 19 | }) 20 | export class UsersRoutingModule {} 21 | -------------------------------------------------------------------------------- /ui/src/app/modules/users/users-support/users-support.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core' 2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 3 | import { TranslatePipe } from '@ngx-translate/core' 4 | 5 | import { SupportBannerComponent } from '@/app/core/components/support-banner/support-banner.component' 6 | 7 | @Component({ 8 | templateUrl: './users-support.component.html', 9 | standalone: true, 10 | imports: [TranslatePipe, SupportBannerComponent], 11 | }) 12 | export class UsersSupportComponent { 13 | private $activeModal = inject(NgbActiveModal) 14 | 15 | public dismissModal() { 16 | this.$activeModal.dismiss('Dismiss') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/modules/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number 3 | name: string 4 | username: string 5 | admin: boolean 6 | otpActive: boolean 7 | password?: string 8 | passwordConfirm?: string 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/modules/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { UsersRoutingModule } from '@/app/modules/users/users-routing.module' 4 | import { UsersResolver } from '@/app/modules/users/users.resolver' 5 | 6 | @NgModule({ 7 | imports: [ 8 | UsersRoutingModule, 9 | ], 10 | providers: [ 11 | UsersResolver, 12 | ], 13 | }) 14 | export class UsersModule {} 15 | -------------------------------------------------------------------------------- /ui/src/app/modules/users/users.resolver.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core' 2 | import { Resolve, Router } from '@angular/router' 3 | import { TranslateService } from '@ngx-translate/core' 4 | import { ToastrService } from 'ngx-toastr' 5 | import { firstValueFrom } from 'rxjs' 6 | 7 | import { ApiService } from '@/app/core/api.service' 8 | 9 | @Injectable() 10 | export class UsersResolver implements Resolve { 11 | private $api = inject(ApiService) 12 | private $router = inject(Router) 13 | private $toastr = inject(ToastrService) 14 | private $translate = inject(TranslateService) 15 | 16 | public async resolve() { 17 | try { 18 | return await firstValueFrom(this.$api.get('/users')) 19 | } catch (error) { 20 | console.error(error) 21 | this.$toastr.error(error.message, this.$translate.instant('toast.title_error')) 22 | void this.$router.navigate(['/']) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/shared/layout/layout.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /ui/src/app/shared/layout/layout.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | margin-left: 60px; 3 | transition: 0.3s; 4 | height: 100%; 5 | 6 | @media (max-width: 767px) { 7 | margin-left: 0; 8 | padding-top: 85px !important; 9 | } 10 | 11 | @media (min-width: 768px) { 12 | height: 100%; 13 | } 14 | } 15 | 16 | a:hover { 17 | text-decoration: none !important; 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/src/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/src/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /ui/src/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/src/assets/bootstrap-5/cssframework/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "stylesheets": [], 3 | "scripts": [] 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/assets/favicon-16x16.png -------------------------------------------------------------------------------- /ui/src/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/assets/favicon-32x32.png -------------------------------------------------------------------------------- /ui/src/assets/hb-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/assets/hb-icon.png -------------------------------------------------------------------------------- /ui/src/assets/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Homebridge", 3 | "short_name": "Homebridge", 4 | "description": "Homebridge is a lightweight NodeJS server that emulates the iOS HomeKit API.", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "orientation": "any", 8 | "background_color": "#57277c", 9 | "theme_color": "#140a33", 10 | "icons": [ 11 | { 12 | "src": "android-chrome-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png", 15 | "purpose": "any maskable" 16 | }, 17 | { 18 | "src": "android-chrome-512x512.png", 19 | "sizes": "512x512", 20 | "type": "image/png", 21 | "purpose": "any maskable" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | // eslint-disable-next-line ts/no-require-imports 3 | serverTarget: require('../../../package.json').version, 4 | production: true, 5 | socket: '', 6 | api: { 7 | base: (() => { 8 | const baseElement = document.querySelector('base') 9 | const baseHref = baseElement?.getAttribute('href') || '/' 10 | return baseHref.endsWith('/') ? `${baseHref}api` : `${baseHref}/api` 11 | })(), 12 | socket: `${(window.location.protocol) === 'http:' ? 'ws://' : 'wss://'}${window.location.host}`, 13 | origin: window.location.origin, 14 | }, 15 | jwt: { 16 | tokenKey: 'access_token', 17 | allowedDomains: [document.location.host], 18 | disallowedRoutes: [(() => { 19 | const baseElement = document.querySelector('base') 20 | const baseHref = baseElement?.getAttribute('href') || '/' 21 | const apiBase = baseHref.endsWith('/') ? `${baseHref}api` : `${baseHref}/api` 22 | return `${window.location.protocol}//${document.location.host}${apiBase}/auth/login` 23 | })()], 24 | }, 25 | apiHttpOptions: {}, 26 | owm: { 27 | appid: 'fec67b55f7f74deaa28df89ba6a60821', 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | // eslint-disable-next-line ts/no-require-imports 8 | serverTarget: require('../../../package.json').version, 9 | production: false, 10 | api: { 11 | base: 'http://localhost:8581/api', 12 | socket: 'http://localhost:8581', 13 | origin: 'http://localhost:8581', 14 | }, 15 | jwt: { 16 | tokenKey: 'access_token', 17 | allowedDomains: ['localhost:8581'], 18 | disallowedRoutes: ['http://localhost:8581/api/auth/login'], 19 | }, 20 | apiHttpOptions: { 21 | withCredentials: true, 22 | }, 23 | owm: { 24 | appid: 'fec67b55f7f74deaa28df89ba6a60821', 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebridge/homebridge-config-ui-x/fab64cf43bfbf2c29c272b647142978c4fb6b7a2/ui/src/favicon.ico -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Homebridge 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ui/src/scss/base/background.scss: -------------------------------------------------------------------------------- 1 | .bg-black { 2 | background-color: #000000 !important; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/scss/base/checkbox.scss: -------------------------------------------------------------------------------- 1 | /* Custom Checkboxes */ 2 | 3 | .hb-uix-switch { 4 | display: block; 5 | position: relative; 6 | padding-left: 35px; 7 | margin-bottom: 12px; 8 | cursor: pointer; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | 14 | input { 15 | position: absolute; 16 | opacity: 0; 17 | cursor: pointer; 18 | height: 0; 19 | width: 0; 20 | } 21 | 22 | .hb-uix-slider { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | height: 25px; 27 | width: 25px; 28 | border: 1px solid; 29 | } 30 | 31 | .hb-uix-slider:after { 32 | content: ''; 33 | position: absolute; 34 | display: none; 35 | } 36 | 37 | input:checked ~ .hb-uix-slider { 38 | border: none; 39 | } 40 | 41 | input:checked ~ .hb-uix-slider:after { 42 | display: block; 43 | } 44 | 45 | .hb-uix-slider:after { 46 | left: 8px; 47 | top: 3px; 48 | width: 9px; 49 | height: 15px; 50 | border: solid white; 51 | border-width: 0 3px 3px 0; 52 | -ms-transform: rotate(45deg); 53 | transform: rotate(45deg); 54 | } 55 | .hb-uix-round { 56 | border-radius: 4px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui/src/scss/base/modal.scss: -------------------------------------------------------------------------------- 1 | .modal-backdrop { 2 | z-index: 1050 !important; 3 | } 4 | 5 | .modal-header { 6 | border-top-left-radius: 0.5rem !important; 7 | border-top-right-radius: 0.5rem !important; 8 | } 9 | 10 | .modal-footer { 11 | border-bottom-left-radius: 0.5rem !important; 12 | border-bottom-right-radius: 0.5rem !important; 13 | } 14 | 15 | .modal-content { 16 | border-radius: 0.7rem !important; 17 | border: none; 18 | line-height: 1.5rem; 19 | font-size: 0.9rem; 20 | font-weight: 300; 21 | 22 | legend { 23 | line-height: 2rem; 24 | } 25 | 26 | ul:not(.list-group) { 27 | padding-inline-start: 20px; 28 | padding-right: 20px; 29 | 30 | li { 31 | margin: 0; 32 | padding: 0; 33 | } 34 | li:not(:last-child) { 35 | margin-bottom: 0.25rem; 36 | } 37 | } 38 | 39 | .list-group-hb { 40 | .list-group-item-hb { 41 | margin-top: 0.5rem; 42 | border-bottom: 1px solid rgba(0, 0, 0, 0.125) !important; 43 | } 44 | } 45 | 46 | .small { 47 | line-height: 1.25rem; 48 | } 49 | 50 | select, 51 | input { 52 | font-size: 1rem; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ui/src/scss/components/editor.scss: -------------------------------------------------------------------------------- 1 | .hb-monaco-editor-line-error { 2 | background: red; 3 | width: 5px !important; 4 | margin-left: 3px; 5 | } 6 | 7 | .hb-plain-text-editor { 8 | box-shadow: 9 | 0 2px 5px 0 rgba(0, 0, 0, 0.16), 10 | 0 2px 10px 0 rgba(0, 0, 0, 0.12); 11 | border: none; 12 | white-space: pre; 13 | font-family: monospace; 14 | overflow-wrap: normal; 15 | overflow-x: scroll; 16 | font-size: 16px; 17 | } 18 | 19 | .hb-editor-block-error { 20 | position: absolute; 21 | background: rgba(255, 231, 186, 0.88); 22 | z-index: 20; 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/scss/components/json-schema-form.scss: -------------------------------------------------------------------------------- 1 | /* Json Schema Form CSS */ 2 | 3 | json-schema-form { 4 | .form-group { 5 | padding-left: 2px; 6 | padding-right: 2px; 7 | } 8 | 9 | .schema-form-fieldset.form-group { 10 | margin-bottom: 0; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/scss/components/terminal.scss: -------------------------------------------------------------------------------- 1 | #plugin-log-output { 2 | min-height: 400px; 3 | background-color: #000000; 4 | padding: 5px 5px 5px 5px; 5 | overflow: hidden; 6 | } 7 | 8 | .no-scrollbars, 9 | .xterm-viewport { 10 | &::-webkit-scrollbar { 11 | display: none; // chrome 12 | } 13 | scrollbar-width: none; // firefox 14 | -ms-overflow-style: none; // edge 15 | } 16 | 17 | // https://github.com/xtermjs/xterm.js/pull/4979/files 18 | .terminal .xterm-rows span { 19 | pointer-events: none; 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/scss/components/toastr.scss: -------------------------------------------------------------------------------- 1 | @import 'ngx-toastr/toastr'; 2 | 3 | @media (max-width: 575px) { 4 | #toast-container { 5 | margin-left: 12px; 6 | width: calc(100% - 24px); 7 | 8 | div { 9 | width: 100%; 10 | } 11 | } 12 | } 13 | 14 | .toast { 15 | font-size: initial !important; 16 | border: initial !important; 17 | backdrop-filter: blur(0) !important; 18 | } 19 | 20 | .toast-title { 21 | font-weight: 300; 22 | } 23 | 24 | .toast-message { 25 | font-weight: 300; 26 | font-size: 0.85rem; 27 | } 28 | 29 | .toast-success { 30 | background-color: #51a351 !important; 31 | } 32 | 33 | .toast-error { 34 | background-color: #bd362f !important; 35 | } 36 | 37 | .toast-info { 38 | background-color: #2f96b4 !important; 39 | } 40 | 41 | .toast-warning { 42 | background-color: #f89406 !important; 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/scss/components/widgets.scss: -------------------------------------------------------------------------------- 1 | /** Gridster **/ 2 | gridster-item { 3 | font-weight: 300; 4 | border: 0; 5 | color: #000000; 6 | background-color: #fff; 7 | border-radius: 0.25rem; 8 | } 9 | 10 | gridster-preview { 11 | background: rgba(224, 224, 224, 0.5); 12 | } 13 | 14 | .widget-value-parent-wrap { 15 | min-width: 85px; 16 | 17 | .widget-value { 18 | font-size: 2rem; 19 | font-weight: 300; 20 | line-height: 1.2; 21 | 22 | @media (max-width: 1500px) { 23 | font-size: 1.75rem; 24 | } 25 | 26 | @media (max-width: 1300px) { 27 | font-size: 1.5rem; 28 | } 29 | } 30 | 31 | .widget-value-label { 32 | @media (max-width: 850px) { 33 | font-size: 13px; 34 | } 35 | } 36 | } 37 | 38 | .qr-code-container { 39 | svg { 40 | position: relative; 41 | height: 100%; 42 | max-height: 100%; 43 | max-width: 100%; 44 | } 45 | } 46 | 47 | .widget-cursor { 48 | cursor: move; 49 | 50 | @media (max-width: 1023px) { 51 | cursor: auto; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | $image-path: '../../img'; 4 | 5 | @font-face { 6 | font-family: 'Font Awesome 7 Free'; 7 | src: local('Font Awesome 7 Free'); 8 | } 9 | 10 | @import '@fortawesome/fontawesome-free/scss/fontawesome.scss'; 11 | @import '@fortawesome/fontawesome-free/scss/solid.scss'; 12 | @import '@fortawesome/fontawesome-free/scss/regular.scss'; 13 | @import '@fortawesome/fontawesome-free/scss/brands.scss'; 14 | @import '@fortawesome/fontawesome-free/scss/v4-shims.scss'; 15 | 16 | @import 'bootstrap/scss/bootstrap'; 17 | 18 | @import '@xterm/xterm/css/xterm.css'; 19 | @import 'nouislider/dist/nouislider.min.css'; 20 | @import 'dragula/dist/dragula.min.css'; 21 | 22 | @import './base/checkbox.scss'; 23 | @import './base/layout.scss'; 24 | @import './base/buttons.scss'; 25 | @import './base/form.scss'; 26 | @import './base/background.scss'; 27 | @import './base/modal.scss'; 28 | 29 | @import './components/toastr.scss'; 30 | @import './components/accessories.scss'; 31 | @import './components/json-schema-form.scss'; 32 | @import './components/sliding-checkbox.scss'; 33 | 34 | @import './components/widgets.scss'; 35 | @import './components/terminal.scss'; 36 | @import './components/editor.scss'; 37 | 38 | @import './themes/themes-light.scss'; 39 | @import './themes/themes-dark.scss'; 40 | -------------------------------------------------------------------------------- /ui/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | // eslint-disable-next-line no-var 3 | declare var module: NodeModule 4 | interface NodeModule { 5 | id: string 6 | } 7 | 8 | declare module 'jwt-decode' { 9 | function decode(token: string): any 10 | namespace decode {} 11 | export = decode 12 | } 13 | -------------------------------------------------------------------------------- /ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "types": [], 6 | "outDir": "./out-tsc/app", 7 | "skipLibCheck": true 8 | }, 9 | "files": [ 10 | "src/main.ts", 11 | "src/polyfills.ts" 12 | ], 13 | "include": [ 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "target": "es2022", 6 | "lib": [ 7 | "es2022", 8 | "dom" 9 | ], 10 | "experimentalDecorators": true, 11 | "baseUrl": "./", 12 | "module": "es2022", 13 | "moduleResolution": "bundler", 14 | "paths": { 15 | "@/*": [ 16 | "src/*" 17 | ] 18 | }, 19 | "declaration": false, 20 | "importHelpers": true, 21 | "outDir": "./dist/out-tsc", 22 | "sourceMap": true, 23 | "esModuleInterop": true 24 | }, 25 | "angularCompilerOptions": { 26 | "disableTypeScriptVersionCheck": true, 27 | "enableI18nLegacyMessageIdFormat": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import swc from 'unplugin-swc' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | coverage: { 7 | include: ['src/**/*.ts'], 8 | }, 9 | fileParallelism: false, 10 | include: ['test/**/*.e2e-spec.ts'], 11 | }, 12 | plugins: [ 13 | // This is required to build the test files with SWC 14 | swc.vite({ 15 | // Explicitly set the module type to avoid inheriting this value from a `.swcrc` config file 16 | module: { 17 | type: 'es6', 18 | }, 19 | }), 20 | ], 21 | }) 22 | --------------------------------------------------------------------------------