diff --git a/CHANGELOG.md b/CHANGELOG.md index b6ce3843..cdf22924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - OVRToolkit as a notification provider - Integration with Home Assistant via MQTT - Additional configuration options for the sleep detector (controller presence, sleeping pose) +- Options for forcing the power state of base stations (Right click the power button). +- Support for base stations that don't report their status. (Newer lighthouses sold through Valve, likely manufactured by HTC) ### Changed diff --git a/src-core/src/lighthouse/mod.rs b/src-core/src/lighthouse/mod.rs index 5706805f..0ce0bf52 100644 --- a/src-core/src/lighthouse/mod.rs +++ b/src-core/src/lighthouse/mod.rs @@ -143,6 +143,10 @@ pub async fn get_device_power_state( Ok(characteristic) => characteristic, Err(err) => return Err(err), }; + // (Raphii): For personal testing purposes, I can pretend one of my basestations doesn't report its status + // if device_id == "BluetoothLE#BluetoothLE48:51:c5:c6:5f:4c-f1:46:cc:56:2e:8c" { + // return Err(LighthouseError::CharacteristicNotFound); + // } let value = match characteristic.read().await { Ok(value) => value, Err(err) => return Err(LighthouseError::FailedToReadCharacteristic(err)), diff --git a/src-overlay-ui/package-lock.json b/src-overlay-ui/package-lock.json index 758e32ad..758f0dbc 100644 --- a/src-overlay-ui/package-lock.json +++ b/src-overlay-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "oyasumivr-overlay-ui", - "version": "1.12.6", + "version": "1.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "oyasumivr-overlay-ui", - "version": "1.12.6", + "version": "1.13.0", "dependencies": { "@sveltekit-i18n/base": "^1.3.5", "@sveltekit-i18n/parser-icu": "^1.0.8", diff --git a/src-ui/app/app.module.ts b/src-ui/app/app.module.ts index 5f96da3f..264eca43 100644 --- a/src-ui/app/app.module.ts +++ b/src-ui/app/app.module.ts @@ -210,6 +210,7 @@ import { SleepDetectionDetectionTabComponent } from './views/dashboard-view/view import { SleepDetectionSleepEnableTabComponent } from './views/dashboard-view/views/sleep-detection-view/tabs/sleep-detection-sleep-enable-tab/sleep-detection-sleep-enable-tab.component'; import { SleepDetectionSleepDisableTabComponent } from './views/dashboard-view/views/sleep-detection-view/tabs/sleep-detection-sleep-disable-tab/sleep-detection-sleep-disable-tab.component'; import { SleepDetectionViewComponent } from './views/dashboard-view/views/sleep-detection-view/sleep-detection-view.component'; +import { DeviceListLhStatePopoverComponent } from './components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component'; [ localeEN, @@ -336,6 +337,7 @@ export function createTranslateLoader(http: HttpClient) { SleepDetectionDetectionTabComponent, SleepDetectionSleepEnableTabComponent, SleepDetectionSleepDisableTabComponent, + DeviceListLhStatePopoverComponent, ], imports: [ CommonModule, diff --git a/src-ui/app/components/device-list/device-list-item/device-list-item.component.html b/src-ui/app/components/device-list/device-list-item/device-list-item.component.html index 1ca1f16f..85089d6d 100644 --- a/src-ui/app/components/device-list/device-list-item/device-list-item.component.html +++ b/src-ui/app/components/device-list/device-list-item/device-list-item.component.html @@ -1,5 +1,5 @@ - -
+ +
@@ -7,44 +7,61 @@
{{ deviceName }}
{{ 'comp.device-list.deviceRole.' + deviceRole | translate }} + >{{ 'comp.device-list.deviceRole.' + deviceRole | translate }}
{{ deviceIdentifier }} + >{{ deviceIdentifier }}
{{ deviceNickname }}
-
+
+
+ + +
-
+
{{ s | translate }}
-
+
+ diff --git a/src-ui/app/components/device-list/device-list-item/device-list-item.component.scss b/src-ui/app/components/device-list/device-list-item/device-list-item.component.scss index a8a56d73..1211cf5a 100644 --- a/src-ui/app/components/device-list/device-list-item/device-list-item.component.scss +++ b/src-ui/app/components/device-list/device-list-item/device-list-item.component.scss @@ -112,6 +112,10 @@ &.power-off { --spinner-color: var(--color-alert-error); } + + &.power-unknown { + --spinner-color: var(--color-text-1); + } } .category-icon { @@ -129,3 +133,4 @@ .device-ignored { opacity: 0.6; } + diff --git a/src-ui/app/components/device-list/device-list-item/device-list-item.component.ts b/src-ui/app/components/device-list/device-list-item/device-list-item.component.ts index 43d91a1e..d4fdbc38 100644 --- a/src-ui/app/components/device-list/device-list-item/device-list-item.component.ts +++ b/src-ui/app/components/device-list/device-list-item/device-list-item.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, Input, OnInit } from '@angular/core'; import { OVRDevice } from 'src-ui/app/models/ovr-device'; import { fade, hshrink, vshrink } from 'src-ui/app/utils/animations'; import { LighthouseConsoleService } from '../../../services/lighthouse-console.service'; @@ -8,7 +8,7 @@ import { EventLogTurnedOffOpenVRDevices, } from '../../../models/event-log-entry'; import { EventLogService } from '../../../services/event-log.service'; -import { LighthouseDevice } from 'src-ui/app/models/lighthouse-device'; +import { LighthouseDevice, LighthouseDevicePowerState } from 'src-ui/app/models/lighthouse-device'; import { LighthouseService } from 'src-ui/app/services/lighthouse.service'; import { AppSettingsService } from 'src-ui/app/services/app-settings.service'; import { firstValueFrom } from 'rxjs'; @@ -19,6 +19,12 @@ import { } from '../device-edit-modal/device-edit-modal.component'; import { ModalService } from 'src-ui/app/services/modal.service'; import { OpenVRService } from '../../../services/openvr.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + ConfirmModalComponent, + ConfirmModalInputModel, + ConfirmModalOutputModel, +} from '../../confirm-modal/confirm-modal.component'; @Component({ selector: 'app-device-list-item', @@ -27,93 +33,56 @@ import { OpenVRService } from '../../../services/openvr.service'; animations: [fade(), vshrink(), hshrink()], }) export class DeviceListItemComponent implements OnInit { - @Input() ovrDevice: OVRDevice | undefined; - @Input() lighthouseDevice: LighthouseDevice | undefined; @Input() icon: string | undefined; - constructor( - private lighthouseConsole: LighthouseConsoleService, - private lighthouse: LighthouseService, - private openvr: OpenVRService, - private eventLog: EventLogService, - private appSettings: AppSettingsService, - private modalService: ModalService - ) {} - - ngOnInit(): void {} - - protected get deviceName(): string { - if (this.ovrDevice) { - return this.ovrDevice.modelNumber; - } - if (this.lighthouseDevice) { - return 'comp.device-list.deviceName.' + this.lighthouseDevice.deviceType; - } - return 'comp.device-list.deviceName.unknown'; + @Input() set ovrDevice(device: OVRDevice | undefined) { + if (!device) return; + this.mode = 'openvr'; + this._lighthouseDevice = undefined; + this._ovrDevice = device; + this.deviceName = device.modelNumber; + this.deviceIdentifier = device.serialNumber; + this.deviceRole = device.handleType; + this.deviceNickname = this.openvr.getDeviceNickname(device); + this.showBattery = Boolean(device.providesBatteryStatus || device.isCharging); + this.isCharging = this.showBattery && device.isCharging; + this.batteryPercentage = this.showBattery ? device.battery * 100 : 0; + this.batteryPercentageString = this.showBattery + ? Math.floor(device.battery * 1000) / 10 + '%' + : 0 + '%'; + this.status = null; + if (device.isTurningOff) this.powerButtonState = 'turn_off_busy'; + else if (device.canPowerOff && device.dongleId) this.powerButtonState = 'turn_off'; + this.isDeviceIgnored = false; + this.cssId = this.sanitizeIdentifierForCSS(device.serialNumber); + this.powerButtonAnchorId = '--anchor-device-pwr-btn-' + this.cssId; + this.showLHStatePopover = false; } - protected get deviceIdentifier(): string { - if (this.ovrDevice) { - return this.ovrDevice.serialNumber; - } - if (this.lighthouseDevice) { - return this.lighthouseDevice.deviceName; + @Input() set lighthouseDevice(device: LighthouseDevice | undefined) { + if (!device) return; + this.mode = 'lighthouse'; + this._lighthouseDevice = device; + this._ovrDevice = undefined; + this.deviceName = 'comp.device-list.deviceName.' + device.deviceType; + this.deviceIdentifier = device.deviceName; + this.deviceRole = undefined; + this.deviceNickname = this.lighthouse.getDeviceNickname(device); + this.showBattery = false; + this.isCharging = false; + this.batteryPercentage = 100; + this.batteryPercentageString = '100%'; + switch (device.powerState) { + case 'unknown': + this.status = null; + break; + default: + this.status = 'comp.device-list.lighthouseStatus.' + device.powerState; + break; } - return 'unknown'; - } - - protected get deviceRole(): string | undefined { - return this.ovrDevice?.handleType; - } - - protected get deviceNickname(): string | null { - if (this.ovrDevice) { - return this.openvr.getDeviceNickname(this.ovrDevice); - } - if (this.lighthouseDevice) { - return this.lighthouse.getDeviceNickname(this.lighthouseDevice); - } - return null; - } - - protected get showBattery(): boolean { - return Boolean( - this.ovrDevice && (this.ovrDevice.providesBatteryStatus || this.ovrDevice.isCharging) - ); - } - - protected get isCharging(): boolean { - return this.showBattery && this.ovrDevice!.isCharging; - } - - protected get batteryPercentage(): number { - return this.showBattery ? this.ovrDevice!.battery * 100 : 0; - } - - protected get batteryPercentageString(): string { - return this.showBattery ? Math.floor(this.ovrDevice!.battery * 1000) / 10 + '%' : 0 + '%'; - } - - protected get status(): string | null { - if (this.lighthouseDevice) { - return 'comp.device-list.lighthouseStatus.' + this.lighthouseDevice.powerState; - } - return null; - } - - protected get powerButtonState(): - | 'hide' - | 'turn_off' - | 'turn_on' - | 'turn_off_busy' - | 'turn_on_busy' { - if (this.ovrDevice) { - if (this.ovrDevice.isTurningOff) return 'turn_off_busy'; - if (this.ovrDevice.canPowerOff && this.ovrDevice.dongleId) return 'turn_off'; - } - if (this.lighthouseDevice) { - if (this.lighthouseDevice.transitioningToPowerState) { - switch (this.lighthouseDevice.transitioningToPowerState) { + this.powerButtonState = (() => { + if (device.transitioningToPowerState) { + switch (device.transitioningToPowerState) { case 'on': return 'turn_on_busy'; case 'sleep': @@ -123,10 +92,10 @@ export class DeviceListItemComponent implements OnInit { return 'turn_on_busy'; case 'unknown': default: - return 'hide'; + return 'turn_on_off_busy'; } } else { - switch (this.lighthouseDevice.powerState) { + switch (device.powerState) { case 'on': return 'turn_off'; case 'sleep': @@ -136,36 +105,113 @@ export class DeviceListItemComponent implements OnInit { return 'turn_on_busy'; case 'unknown': default: - return 'hide'; + return 'turn_on_off'; } } + })(); + this.isDeviceIgnored = this.lighthouse.isDeviceIgnored(device); + this.cssId = this.sanitizeIdentifierForCSS(device.id); + this.powerButtonAnchorId = '--anchor-device-pwr-btn-' + this.cssId; + } + + mode?: 'lighthouse' | 'openvr'; + deviceName = ''; + deviceIdentifier = ''; + deviceRole: string | undefined = undefined; + deviceNickname: string | null = null; + showBattery = false; + isCharging = false; + batteryPercentage = 100; + batteryPercentageString = '100%'; + status: string | null = null; + powerButtonState: + | 'hide' + | 'turn_on_off' + | 'turn_on_off_busy' + | 'turn_off' + | 'turn_on' + | 'turn_off_busy' + | 'turn_on_busy' = 'hide'; + isDeviceIgnored = false; + powerButtonAnchorId = ''; + showLHStatePopover = false; + cssId: string = ''; + _lighthouseDevice?: LighthouseDevice; + _ovrDevice?: OVRDevice; + + constructor( + private lighthouseConsole: LighthouseConsoleService, + private lighthouse: LighthouseService, + private openvr: OpenVRService, + private eventLog: EventLogService, + private appSettings: AppSettingsService, + private destroyRef: DestroyRef, + private modalService: ModalService + ) {} + + ngOnInit(): void { + // Retrigger the setters when devices have updated + this.openvr.devices.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((devices) => { + if (this._ovrDevice) this.ovrDevice = this._ovrDevice; + }); + this.lighthouse.devices.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((devices) => { + if (this._lighthouseDevice) this.lighthouseDevice = this._lighthouseDevice; + }); + } + + async onForceLHState(state: LighthouseDevicePowerState) { + this.showLHStatePopover = false; + if (state === 'on' && this._lighthouseDevice?.powerState === 'on') { + const result = await firstValueFrom( + this.modalService.addModal( + ConfirmModalComponent, + { + title: 'comp.device-list.forceOnBootingWarning.title', + message: 'comp.device-list.forceOnBootingWarning.message', + confirmButtonText: 'comp.device-list.lhForceState.on', + } + ) + ); + if (!result || !result.confirmed) return; } - return 'hide'; + this.eventLog.logEvent({ + type: 'lighthouseSetPowerState', + reason: 'MANUAL', + state, + devices: 'SINGLE', + } as EventLogLighthouseSetPowerState); + await this.lighthouse.setPowerState(this._lighthouseDevice!, state, true); + } + + rightClickDevicePowerButton() { + this.showLHStatePopover = !this.showLHStatePopover; } async clickDevicePowerButton() { - if (this.ovrDevice) { - await this.lighthouseConsole.turnOffDevices([this.ovrDevice]); + if (this.mode === 'openvr') { + await this.lighthouseConsole.turnOffDevices([this._ovrDevice!]); this.eventLog.logEvent({ type: 'turnedOffOpenVRDevices', reason: 'MANUAL', devices: (() => { - switch (this.ovrDevice.class) { + switch (this._ovrDevice!.class) { case 'Controller': return 'CONTROLLER'; case 'GenericTracker': return 'TRACKER'; default: error( - `[DeviceListItem] Couldn't determine device class for event log entry (${this.ovrDevice.class})` + `[DeviceListItem] Couldn't determine device class for event log entry (${ + this._ovrDevice!.class + })` ); return 'VARIOUS'; } })(), } as EventLogTurnedOffOpenVRDevices); } - if (this.lighthouseDevice) { - switch (this.lighthouseDevice.powerState) { + if (this.mode === 'lighthouse') { + switch (this._lighthouseDevice!.powerState) { case 'on': { const state = (await firstValueFrom(this.appSettings.settings)).lighthousePowerOffState; this.eventLog.logEvent({ @@ -174,7 +220,7 @@ export class DeviceListItemComponent implements OnInit { state, devices: 'SINGLE', } as EventLogLighthouseSetPowerState); - await this.lighthouse.setPowerState(this.lighthouseDevice, state); + await this.lighthouse.setPowerState(this._lighthouseDevice!, state); break; } case 'sleep': @@ -185,10 +231,12 @@ export class DeviceListItemComponent implements OnInit { state: 'on', devices: 'SINGLE', } as EventLogLighthouseSetPowerState); - await this.lighthouse.setPowerState(this.lighthouseDevice, 'on'); + await this.lighthouse.setPowerState(this._lighthouseDevice!, 'on'); break; - case 'booting': case 'unknown': + this.rightClickDevicePowerButton(); + break; + case 'booting': default: break; } @@ -216,7 +264,13 @@ export class DeviceListItemComponent implements OnInit { .subscribe(); } - isDeviceIgnored() { - return this.lighthouseDevice && this.lighthouse.isDeviceIgnored(this.lighthouseDevice); + private sanitizeIdentifierForCSS(serialNumber: string) { + return serialNumber.replace(/[^a-zA-Z0-9]/g, ''); + } + + onClickOutsideLHStatePopover($event: MouseEvent) { + const targetId = ($event.target as HTMLElement).id; + if (targetId === 'btn-power-' + this.cssId) return; + this.showLHStatePopover = false; } } diff --git a/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.html b/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.html new file mode 100644 index 00000000..d4db45a2 --- /dev/null +++ b/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.html @@ -0,0 +1,41 @@ +
+
+ {{ label | translate }} +
+
+ + + +
+
+
diff --git a/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.scss b/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.scss new file mode 100644 index 00000000..08542c6f --- /dev/null +++ b/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.scss @@ -0,0 +1,50 @@ +@import 'shadows'; + +:host { + position: absolute; + right: anchor(left); + align-self: anchor-center; + + display: flex; + flex-direction: row; + align-items: stretch; + transition: all .15s ease; +} + +.pane { + @include shadow(5, true); + background: var(--color-surface-0); + border: 1px solid var(--color-surface-3); + display: flex; + flex-direction: row; + align-items: center; + color: var(--color-text-1); + padding: 0; + + .action-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; + padding: 0.75em; + button { + &:not(:last-child) { + margin-right: 0.5em; + } + } + } + + .label { + padding-left: 1em; + display: flex; + flex-direction: column; + text-align: center; + align-items: center; + justify-content: center; + white-space: nowrap; + } +} + +.spacer { + width: 0.5em; +} diff --git a/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.ts b/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.ts new file mode 100644 index 00000000..65c6784d --- /dev/null +++ b/src-ui/app/components/device-list/device-list-lh-state-popover/device-list-lh-state-popover.component.ts @@ -0,0 +1,24 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { hshrink, vshrink } from '../../../utils/animations'; +import { LighthouseDevicePowerState } from '../../../models/lighthouse-device'; + +@Component({ + selector: 'app-device-list-lh-state-popover', + templateUrl: './device-list-lh-state-popover.component.html', + styleUrls: ['./device-list-lh-state-popover.component.scss'], + animations: [hshrink()], +}) +export class DeviceListLhStatePopoverComponent implements OnInit { + hoverAction = ''; + active = false; + @Output() action = new EventEmitter(); + + ngOnInit() { + setTimeout(() => (this.active = true), 150); + } + + setHoverAction(action: string) { + if (!this.active) return; + this.hoverAction = action; + } +} diff --git a/src-ui/app/components/device-list/device-list.component.html b/src-ui/app/components/device-list/device-list.component.html index d488ebcd..8525ce8c 100644 --- a/src-ui/app/components/device-list/device-list.component.html +++ b/src-ui/app/components/device-list/device-list.component.html @@ -48,14 +48,24 @@ +
+ + +
- (d.powerState === 'standby' || d.powerState === 'sleep') && + (d.powerState === 'standby' || d.powerState === 'sleep' || d.powerState === 'unknown') && !this.lighthouse.isDeviceIgnored(d) ); this.eventLog.logEvent({ @@ -290,7 +300,9 @@ export class DeviceListComponent implements OnInit { const powerOffState = (await firstValueFrom(this.appSettings.settings)) .lighthousePowerOffState; const devices = category.devices.filter( - (d) => d.powerState === 'on' && !this.lighthouse.isDeviceIgnored(d) + (d) => + (d.powerState === 'on' || d.powerState === 'unknown' || d.powerState === 'booting') && + !this.lighthouse.isDeviceIgnored(d) ); this.eventLog.logEvent({ type: 'lighthouseSetPowerState', @@ -324,4 +336,30 @@ export class DeviceListComponent implements OnInit { async scanForLighthouses() { this.lighthouse.scan(); } + + onClickOutsideLHStatePopover($event: MouseEvent) { + const targetId = ($event.target as HTMLElement).id; + if (targetId !== 'btn-lh-bulk-power') { + this.showLHStatePopover = false; + } + } + + async onForceLHState(state: LighthouseDevicePowerState) { + let devices = this.deviceCategories + .filter((c) => c.type === 'Lighthouse') + .map((c) => (c as LighthouseDisplayCategory).devices) + .flat() + .filter((d) => !this.lighthouse.isDeviceIgnored(d)); + if (state === 'on') { + devices = devices.filter((d) => d.powerState !== 'on'); + } + if (!devices.length) return; + this.eventLog.logEvent({ + type: 'lighthouseSetPowerState', + reason: 'MANUAL', + state, + devices: 'ALL', + } as EventLogLighthouseSetPowerState); + await Promise.all(devices.map(async (device) => this.lighthouse.setPowerState(device, state))); + } } diff --git a/src-ui/app/directives/click-outside.directive.ts b/src-ui/app/directives/click-outside.directive.ts index f59df23c..086d4d40 100644 --- a/src-ui/app/directives/click-outside.directive.ts +++ b/src-ui/app/directives/click-outside.directive.ts @@ -9,6 +9,7 @@ export class ClickOutsideDirective { @Output() clickOutside = new EventEmitter(); @HostListener('document:click', ['$event', '$event.target']) + @HostListener('document:contextmenu', ['$event', '$event.target']) public onClick(event: MouseEvent, targetElement: HTMLElement): void { if (!targetElement) { return; diff --git a/src-ui/app/services/hotkey-handler.service.ts b/src-ui/app/services/hotkey-handler.service.ts index c34a223c..d2576937 100644 --- a/src-ui/app/services/hotkey-handler.service.ts +++ b/src-ui/app/services/hotkey-handler.service.ts @@ -126,7 +126,7 @@ export class HotkeyHandlerService { state: 'on', devices: 'ALL', } as EventLogLighthouseSetPowerState); - } else if (!devices.filter((d) => d.powerState !== 'on').length) { + } else if (!devices.filter((d) => d.powerState !== 'on' && d.powerState != 'unknown').length) { // Power off const devicesToPowerOff = devices.filter((d) => !['standby', 'sleep'].includes(d.powerState)); devicesToPowerOff.forEach((d) => { diff --git a/src-ui/app/services/lighthouse.service.ts b/src-ui/app/services/lighthouse.service.ts index c98dce63..8ce19aa6 100644 --- a/src-ui/app/services/lighthouse.service.ts +++ b/src-ui/app/services/lighthouse.service.ts @@ -102,13 +102,18 @@ export class LighthouseService { await invoke('lighthouse_start_scan', { duration: DEFAULT_SCAN_DURATION }); } - public async setPowerState(device: LighthouseDevice, powerState: LighthouseDevicePowerState) { - if (powerState === device.powerState) return; - device.transitioningToPowerState = ['on', 'sleep', 'standby'].includes(powerState) - ? powerState - : undefined; - this._devices.next(this._devices.value); - await invoke('lighthouse_set_device_power_state', { deviceId: device.id, powerState }); + public async setPowerState( + device: LighthouseDevice, + powerState: LighthouseDevicePowerState, + force = false + ) { + if (!force) { + device.transitioningToPowerState = ['on', 'sleep', 'standby'].includes(powerState) + ? powerState + : undefined; + this._devices.next(this._devices.value); + } + invoke('lighthouse_set_device_power_state', { deviceId: device.id, powerState }); // Wait for state to change (timeout after 10 seconds) await firstValueFrom( merge( diff --git a/src-ui/app/services/mqtt/integrations/base-station.mqtt-integration.service.ts b/src-ui/app/services/mqtt/integrations/base-station.mqtt-integration.service.ts index 21f84ba4..c7017e37 100644 --- a/src-ui/app/services/mqtt/integrations/base-station.mqtt-integration.service.ts +++ b/src-ui/app/services/mqtt/integrations/base-station.mqtt-integration.service.ts @@ -80,10 +80,7 @@ export class BaseStationMqttIntegrationService { topicPath: `device/${id}`, displayName: 'Power', value: device.powerState === 'on' || device.powerState === 'booting', - available: - device.powerState === 'on' || - device.powerState === 'sleep' || - device.powerState === 'standby', + available: true, device: deviceDesc, }); @@ -109,10 +106,6 @@ export class BaseStationMqttIntegrationService { private async updateDevice(device: LighthouseDevice) { const id = this.sanitizedId(device.id); - await this.mqtt.setPropertyAvailability( - id, - device.powerState === 'on' || device.powerState === 'sleep' || device.powerState === 'standby' - ); await this.mqtt.setTogglePropertyValue( id, device.powerState === 'on' || device.powerState === 'booting' diff --git a/src-ui/app/services/osc-control/methods/command.osc-method.ts b/src-ui/app/services/osc-control/methods/command.osc-method.ts index 6d24f72b..1c3de696 100644 --- a/src-ui/app/services/osc-control/methods/command.osc-method.ts +++ b/src-ui/app/services/osc-control/methods/command.osc-method.ts @@ -111,7 +111,7 @@ export class CommandOscMethod extends OscMethod { private async handleTurnOnAllLighthouses() { const devices = await firstValueFrom(this.lighthouse.devices).then((devices) => - devices.filter((d) => !this.lighthouse.isDeviceIgnored(d)) + devices.filter((d) => !this.lighthouse.isDeviceIgnored(d) && d.powerState !== 'on') ); for (const device of devices) { await this.lighthouse.setPowerState(device, 'on'); diff --git a/src-ui/app/services/power-automations/turn-off-lighthouses-on-steamvr-stop-automation.service.ts b/src-ui/app/services/power-automations/turn-off-lighthouses-on-steamvr-stop-automation.service.ts index 83ed245e..cf5022ec 100644 --- a/src-ui/app/services/power-automations/turn-off-lighthouses-on-steamvr-stop-automation.service.ts +++ b/src-ui/app/services/power-automations/turn-off-lighthouses-on-steamvr-stop-automation.service.ts @@ -101,6 +101,7 @@ export class TurnOffLighthousesOnSteamVRStopAutomationService { if (!this.config.enabled) return; if ((await firstValueFrom(this.openvr.status)) !== 'INACTIVE') return; switch (device.powerState) { + case 'unknown': case 'on': case 'booting': { const offPowerState = await firstValueFrom( @@ -109,13 +110,6 @@ export class TurnOffLighthousesOnSteamVRStopAutomationService { await this.lighthouse.setPowerState(device, offPowerState); break; } - case 'unknown': { - // Attempt again in two seconds - if (attempt < 5) { - setTimeout(() => this.handleNewDevice(device, attempt + 1), 2000); - } - break; - } case 'sleep': case 'standby': default: diff --git a/src-ui/app/services/power-automations/turn-on-lighthouses-on-oyasumi-start-automation.service.ts b/src-ui/app/services/power-automations/turn-on-lighthouses-on-oyasumi-start-automation.service.ts index 9de05850..5fac10c1 100644 --- a/src-ui/app/services/power-automations/turn-on-lighthouses-on-oyasumi-start-automation.service.ts +++ b/src-ui/app/services/power-automations/turn-on-lighthouses-on-oyasumi-start-automation.service.ts @@ -58,7 +58,8 @@ export class TurnOnLighthousesOnOyasumiStartAutomationService { !this.seenDevices.includes(lighthouse.id) && (lighthouse.powerState === 'sleep' || lighthouse.powerState === 'standby' || - lighthouse.powerState === 'booting') + lighthouse.powerState === 'booting' || + lighthouse.powerState === 'unknown') ); lighthouses.forEach((d) => this.seenDevices.push(d.id)); if (devices.length) { diff --git a/src-ui/app/services/power-automations/turn-on-lighthouses-on-steamvr-start-automation.service.ts b/src-ui/app/services/power-automations/turn-on-lighthouses-on-steamvr-start-automation.service.ts index d399719f..56423b38 100644 --- a/src-ui/app/services/power-automations/turn-on-lighthouses-on-steamvr-start-automation.service.ts +++ b/src-ui/app/services/power-automations/turn-on-lighthouses-on-steamvr-start-automation.service.ts @@ -61,7 +61,8 @@ export class TurnOnLighthousesOnSteamVRStartAutomationService { (d) => (d.powerState === 'sleep' || d.powerState === 'standby' || - d.powerState === 'booting') && + d.powerState === 'booting' || + d.powerState === 'unknown') && !this.lighthouse.isDeviceIgnored(d) ); if (devices.length) { diff --git a/src-ui/app/utils/browser-utils.ts b/src-ui/app/utils/browser-utils.ts new file mode 100644 index 00000000..3af79672 --- /dev/null +++ b/src-ui/app/utils/browser-utils.ts @@ -0,0 +1,19 @@ +export function disableDefaultContextMenu() { + document.addEventListener( + 'contextmenu', + (e) => { + e.preventDefault(); + return false; + }, + { capture: true } + ); + + document.addEventListener( + 'selectstart', + (e) => { + e.preventDefault(); + return false; + }, + { capture: true } + ); +} diff --git a/src-ui/assets/i18n/en.json b/src-ui/assets/i18n/en.json index 5d7da03b..d813a81e 100644 --- a/src-ui/assets/i18n/en.json +++ b/src-ui/assets/i18n/en.json @@ -373,6 +373,15 @@ "WristLeft": "Left Wrist", "WristRight": "Right Wrist" }, + "forceOnBootingWarning": { + "title": "Warning", + "message": "It looks like this base station is already on.\n\nIf you force a base station to turn on while it's already on, it usually will get stuck in its booting state. If this happens, you can fix this by putting it back to sleep or standby and then turning it on again.\n\nAre you sure you want to force this base station to turn on?" + }, + "lhForceState": { + "on": "Force On", + "sleep": "Force Sleep", + "standby": "Force Standby" + }, "lighthouseStatus": { "booting": "Booting", "on": "Active", diff --git a/src-ui/assets/i18n/ja.json b/src-ui/assets/i18n/ja.json index 0947225e..b27d4d5b 100644 --- a/src-ui/assets/i18n/ja.json +++ b/src-ui/assets/i18n/ja.json @@ -2319,4 +2319,4 @@ }, "title": "VRChatマイクミュートの自動化" } -} +} \ No newline at end of file diff --git a/src-ui/assets/i18n/ru.json b/src-ui/assets/i18n/ru.json index 15c1a22f..e685ae9b 100644 --- a/src-ui/assets/i18n/ru.json +++ b/src-ui/assets/i18n/ru.json @@ -2319,4 +2319,4 @@ }, "title": "Автоматизация отключения микрофона VRChat" } -} +} \ No newline at end of file diff --git a/src-ui/main.ts b/src-ui/main.ts index 4c50318c..4a441b74 100644 --- a/src-ui/main.ts +++ b/src-ui/main.ts @@ -6,6 +6,7 @@ import { environment } from './environments/environment'; import { attachConsole, error, info } from 'tauri-plugin-log-api'; import { getVersion } from './app/utils/app-utils'; import { FLAVOUR } from './build'; +import { disableDefaultContextMenu } from './app/utils/browser-utils'; if (environment.production) { enableProdMode(); @@ -19,6 +20,8 @@ getVersion().then((version) => { info('[Oyasumi] Starting OyasumiVR v' + version + '-' + FLAVOUR); }); +disableDefaultContextMenu(); + platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => error(err)); diff --git a/src-ui/styles/buttons.scss b/src-ui/styles/buttons.scss index 1d81bddb..026666db 100644 --- a/src-ui/styles/buttons.scss +++ b/src-ui/styles/buttons.scss @@ -29,6 +29,11 @@ button.btn-power { background: var(--color-alert-success); color: var(--color-on-alert-success); } + + &.power-unknown { + background: var(--color-text-1); + color: var(--color-surface-1); + } } &:active:after { @@ -55,6 +60,10 @@ button.btn-power { &.power-on { color: var(--color-alert-success); } + + &.power-unknown { + color: var(--color-text-1); + } } a.btn,