diff --git a/server/app.js b/server/app.js index e8397a1..14c57af 100644 --- a/server/app.js +++ b/server/app.js @@ -328,6 +328,32 @@ app.delete('/api/config/remote/:address', async (req, res, next) => { } }) + +app.post('/api/remote/:address/wake', async (req, res, next) => { + const address = req.params.address; + let user = REMOTE_USER + if (req.body?.user) + user = req.body?.user; + let broadcast = req.query.broadcast; + const configFile = getConfigFile(); + const remoteEntry = configFile?.remotes?.find(remote => remote.address === address); + if (!remoteEntry) + { + res.status(404).json(address); + return; + } + const remote = new Remote(remoteEntry.address, remoteEntry.port, remoteEntry.user, remoteEntry.token, + remoteEntry.api_key, remoteEntry.mac_address); + try { + console.log("Wake on lan", remote.mac_address); + await remote.wakeOnLan(broadcast); + res.status(200).end(); + } catch (error) + { + errorHandler(error, req, res, next); + } +}) + app.post('/api/remote/:address/system', async (req, res, next) => { const address = req.params.address; let user = REMOTE_USER diff --git a/server/package-lock.json b/server/package-lock.json index d0eba4f..faf1750 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "server", - "version": "1.6.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "server", - "version": "1.6.0", + "version": "2.0.1", "dependencies": { "commander": "^12.1.0", "cookie-parser": "~1.4.4", @@ -23,6 +23,7 @@ "open": "^10.1.0", "process": "^0.11.10", "rimraf": "^6.0.1", + "wake-on-lan": "^0.1.0", "ws": "^8.18.0" }, "devDependencies": { @@ -696,6 +697,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1052,6 +1103,14 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1369,6 +1428,25 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2551,6 +2629,35 @@ "node": ">= 0.8" } }, + "node_modules/vow": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/vow/-/vow-0.4.20.tgz", + "integrity": "sha512-YYoSYXUYABqY08D/WrjcWJxJSErcILRRTQpcPyUc0SFfgIPKSUFzVt7u1HC3TXGJZM/qhsSjCLNQstxqf7asgQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/wake-on-lan": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wake-on-lan/-/wake-on-lan-0.1.0.tgz", + "integrity": "sha512-tVl8HjYvix9K4p70uJAflhmCxy/N1+KfGOFY9SK60YL9lAKqIULX/sNj9ZecgC+YoONLjT98aL/82dJCr8RiqQ==", + "dependencies": { + "chalk": "^1.1.0", + "commander": "^2.8.1", + "vow": "^0.4.10" + }, + "bin": { + "wake-on-lan": "bin/wake-on-lan" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/wake-on-lan/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/server/package.json b/server/package.json index e13cb75..839cb28 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ "open": "^10.1.0", "process": "^0.11.10", "rimraf": "^6.0.1", + "wake-on-lan": "^0.1.0", "ws": "^8.18.0" }, "devDependencies": { diff --git a/server/remote.js b/server/remote.js index 044c9b3..4c20458 100644 --- a/server/remote.js +++ b/server/remote.js @@ -3,8 +3,7 @@ import fs from "node:fs"; import { readdir } from 'node:fs/promises'; import path from "path"; import {pipeline as streamPipeline} from 'node:stream/promises'; -import * as url from "node:url"; -import * as stream from "node:stream"; +import wakeonlan from "wake-on-lan"; const SystemCommand = { STANDBY: 'STANDBY', @@ -27,14 +26,16 @@ export class Remote remote_name; protocol = 'http://'; resources_path = ''; + mac_address; - constructor(address, port, user, token, api_key) { + constructor(address, port, user, token, api_key, mac_address) { this.address = address; this.port = port; this.api_key = api_key; this.user = user; this.token = token; + this.mac_address = mac_address; } async getResources(type, resources_directory) { @@ -96,6 +97,7 @@ export class Remote if (this.api_key) data.api_key = this.api_key; if (this.api_key_name) data.api_key_name = this.api_key_name; if (this.valid_to) data.valid_to = this.valid_to; + if (this.mac_address) data.mac_address = this.mac_address; return data; } @@ -143,6 +145,7 @@ export class Remote if (res?.body) resBody = JSON.parse(res.body); console.log('Get remote info :', resBody); this.remote_name = resBody.device_name; + this.mac_address = resBody.address; return resBody.device_name; } catch (err) { console.error('Error', err, res?.body); @@ -545,6 +548,23 @@ export class Remote return JSON.parse(res.body); } + async wakeOnLan(broadcast = null) + { + if (!this.mac_address) { + console.error("Cannot wakeonlan, no mac address defined"); + throw new Error("MAC address not defined"); + } + if (broadcast) + await wakeonlan(this.mac_address, {ip:broadcast}) + else { + await wakeonlan(this.mac_address, {ip: '0.0.0.0'}); + try { + let mask = this.address.split('.').slice(0, 3).join('.')+'.0'; + await wakeonlan(this.mac_address, {ip: mask}); + } catch (error) {} + } + } + async getBackup(response) { const options = { headers: this.getHeaders(), throwHttpErrors: false}; diff --git a/src/app/active-entities/active-entities.component.css b/src/app/active-entities/active-entities.component.css index 1c903ee..d87418b 100644 --- a/src/app/active-entities/active-entities.component.css +++ b/src/app/active-entities/active-entities.component.css @@ -16,6 +16,10 @@ padding: 5px; } +.disconnected-state { + cursor: pointer; +} + .media-card-border:hover { background: rgba(0, 0, 0, 0.73); border: 2px solid rgb(30, 31, 34); @@ -40,14 +44,15 @@ .message-panel .p-message .p-message-wrapper { padding: 0.1rem 0.1rem; + height: 25px; } .message-panel .p-message .p-message-close { width: 0.9rem; height: 0.9rem; +} - .message-panel .p-message .p-message-icon { - font-size: 0.9rem; - margin-right: 0.15rem; - }} +.message-panel .p-message-icon { + display:none !important; +} diff --git a/src/app/active-entities/active-entities.component.html b/src/app/active-entities/active-entities.component.html index 4cd63b8..5cd8842 100644 --- a/src/app/active-entities/active-entities.component.html +++ b/src/app/active-entities/active-entities.component.html @@ -1,7 +1,6 @@ + - -
+ [severity]="(remoteWebsocketService.connectionStatus | async) ? 'success' : 'warning'" + [class]="'disconnected-state'" + pTooltip="Click to wake up the remote" + (click)="wakeRemote($event)"/>
- +
{ + this.messageService.add({severity:'success', summary: "Wake on lan command sent", key: 'activeEntities'}); + this.cdr.detectChanges(); + }, + error: error => { + this.messageService.add({severity:'error', summary: "Wake on lan command sent", key: 'activeEntities'}); + this.cdr.detectChanges(); + } + }); + this.server.wakeRemote(this.selectedRemote, "255.255.255.0").subscribe({}); + } } diff --git a/src/app/active-entities/media-entity/media-entity.component.css b/src/app/active-entities/media-entity/media-entity.component.css index 02836f8..c2654f2 100644 --- a/src/app/active-entities/media-entity/media-entity.component.css +++ b/src/app/active-entities/media-entity/media-entity.component.css @@ -8,7 +8,7 @@ color: #fbf100; } -.media-image +.media-image2 { display: block; max-width:250px; @@ -20,6 +20,18 @@ z-index: 10; } +.media-image +{ + display: block; + max-width:100%; + max-height:100%; + width: auto; + height: auto; + transform-origin: top left; + transition: all 1s ease-in-out; + z-index: 10; +} + .media-image:hover, focus { transform: scale(1.4); } diff --git a/src/app/active-entities/media-entity/media-entity.component.html b/src/app/active-entities/media-entity/media-entity.component.html index 8c36c9d..75d5e9b 100644 --- a/src/app/active-entities/media-entity/media-entity.component.html +++ b/src/app/active-entities/media-entity/media-entity.component.html @@ -1,31 +1,29 @@ -
+
-
+

{{remoteWebsocketService.getEntityName(mediaEntity)}}

+ [alt]="mediaEntity.new_state!.attributes!.media_title!"/> + [alt]="mediaEntity.new_state!.attributes!.media_title!"/>
-
+ [style]="{'width': '100%','max-width': mediaEntity.new_state?.attributes?.media_image_url ? (300*scale)+'px' : (800*scale)+'px'}"> +

{{remoteWebsocketService.getEntityName(mediaEntity)}}

-
+
@@ -102,8 +100,9 @@ (click)="mediaAction(mediaEntity, 'media_player.volume_up')" icon="icon icon-plus" size="small" pTooltip="Volume +" [rounded]="true" [outlined]="true" class="uc-button"/> + [textValue]="'Volume '+Helper.getNumber(mediaEntity.new_state!.attributes!.volume!)+'%'+ + (Helper.isMuted(mediaEntity) ? ' (muted)' : '')" + [editable]="checkFeature(mediaEntity, 'volume')" [secondaryState]="Helper.isMuted(mediaEntity)"/>
diff --git a/src/app/active-entities/media-entity/media-entity.component.ts b/src/app/active-entities/media-entity/media-entity.component.ts index af321c6..f7cd777 100644 --- a/src/app/active-entities/media-entity/media-entity.component.ts +++ b/src/app/active-entities/media-entity/media-entity.component.ts @@ -8,7 +8,7 @@ import { TemplateRef, ViewEncapsulation } from '@angular/core'; -import {MediaEntityState, RemoteWebsocketService} from "../../remote-widget/remote-websocket.service"; +import {MediaEntityState, RemoteWebsocketService} from "../../remote-websocket.service"; import {Remote} from "../../interfaces"; import {ServerService} from "../../server.service"; import {Button} from "primeng/button"; @@ -18,6 +18,7 @@ import {ScrollingTextComponent} from "../../controls/scrolling-text/scrolling-te import {SliderComponent} from "../../controls/slider/slider.component"; import {TagModule} from "primeng/tag"; import {TooltipModule} from "primeng/tooltip"; +import {Helper} from "../../helper"; @Component({ selector: 'app-media-entity', @@ -204,8 +205,5 @@ export class MediaEntityComponent implements OnInit, AfterViewInit { cmd_id}).subscribe(); } - getNumber(number: number) { - if (isNaN(number)) return 0; - return Math.round(number); - } + protected readonly Helper = Helper; } diff --git a/src/app/activity-player/activity-player.component.html b/src/app/activity-player/activity-player.component.html index 1fb6ee6..eb2a365 100644 --- a/src/app/activity-player/activity-player.component.html +++ b/src/app/activity-player/activity-player.component.html @@ -34,11 +34,19 @@

{{Helper.getEntityName(activity)}}

(onSelectButton)="handleMessage($event)"/>
+ [selectionMode]="false" [currentPage]="currentPage" [runMode]="true" (onSelectButton)="handleCommand($event)"/> +
+ +
diff --git a/src/app/activity-player/activity-player.component.ts b/src/app/activity-player/activity-player.component.ts index 681edd6..7da5548 100644 --- a/src/app/activity-player/activity-player.component.ts +++ b/src/app/activity-player/activity-player.component.ts @@ -11,14 +11,14 @@ import {DialogModule} from "primeng/dialog"; import {Message, MessageService, PrimeTemplate} from "primeng/api"; import {ActivityViewerComponent} from "../activity-viewer/activity-viewer.component"; import {ServerService} from "../server.service"; -import {RemoteWebsocketService} from "../remote-widget/remote-websocket.service"; -import {Activity, ButtonMapping, EntityCommand, Remote, UIPage} from "../interfaces"; +import {MediaEntityState, RemoteWebsocketService} from "../remote-websocket.service"; +import {Activity, ButtonMapping, Command, Entity, EntityCommand, Remote, UIPage} from "../interfaces"; import {Helper} from "../helper"; import {Button} from "primeng/button"; import {TooltipModule} from "primeng/tooltip"; import {ActivityButtonsComponent, ButtonMode} from "../activity-viewer/activity-buttons/activity-buttons.component"; import {NgIf} from "@angular/common"; -import {catchError, delay, forkJoin, from, map, mergeMap, of} from "rxjs"; +import {catchError, delay, forkJoin, from, map, mergeMap, Observable, of} from "rxjs"; import {ActivityGridComponent} from "../activity-viewer/activity-grid/activity-grid.component"; import {ToastModule} from "primeng/toast"; import {HttpErrorResponse} from "@angular/common/http"; @@ -26,6 +26,7 @@ import {ProgressBarModule} from "primeng/progressbar"; import {PaginationComponent} from "../controls/pagination/pagination.component"; import {RouterLink} from "@angular/router"; import {IconComponent} from "../controls/icon/icon.component"; +import {SliderComponent} from "../controls/slider/slider.component"; @Component({ selector: 'app-activity-player', @@ -43,7 +44,8 @@ import {IconComponent} from "../controls/icon/icon.component"; ProgressBarModule, PaginationComponent, RouterLink, - IconComponent + IconComponent, + SliderComponent ], templateUrl: './activity-player.component.html', styleUrl: './activity-player.component.css', @@ -56,16 +58,20 @@ export class ActivityPlayerComponent { configEntityCommands: EntityCommand[] | undefined; @Input('remote') set _remote(value: Remote | undefined) { this.remote = value; - if (this.remote) + if (this.remote) { this.server.getConfigEntityCommands(this.remote).subscribe(entityCommands => { this.configEntityCommands = entityCommands; this.cdr.detectChanges(); - }) + }); + this.update(); + } } activity: Activity | undefined; @Input("activity") set _activity (activity: Activity | undefined) { this.activity = activity; + console.log("Play activity", this.activity); this.currentPage = activity?.options?.user_interface?.pages?.[0]; + this.update(); this.cdr.detectChanges(); } @Input() visible = false; @@ -76,9 +82,29 @@ export class ActivityPlayerComponent { currentPage: UIPage | undefined; progress = 0; progressDetail: string | undefined; + volumeEntity: MediaEntityState | undefined; constructor(private server:ServerService, protected remoteWebsocketService: RemoteWebsocketService, - private cdr:ChangeDetectorRef, private messageService: MessageService) { } + private cdr:ChangeDetectorRef, private messageService: MessageService) { + this.remoteWebsocketService.onMediaStateChange().subscribe(mediaStates => { + if (!this.volumeEntity) return; + const state = mediaStates.find(item => item.entity_id === this.volumeEntity!.entity_id!); + if (state) { + this.volumeEntity = state; + this.cdr.detectChanges(); + } + }) + } + + update() { + if (!this.remote || !this.activity) return; + this.getVolumeEntity().subscribe(mediaState => { + if (!mediaState) return; + console.log("Volume entity", mediaState); + this.volumeEntity = mediaState; + this.cdr.detectChanges(); + }) + } protected readonly Helper = Helper; @@ -159,13 +185,65 @@ export class ActivityPlayerComponent { this.cdr.detectChanges(); } + handleCommand($event: {command: Command, mode: ButtonMode, severity: "success" | "error", error?: string}) { + let message = "Short press "; + if ($event.mode === ButtonMode.ShortPress) message = `Short press `; + else if ($event.mode === ButtonMode.LongPress) message = `Long press `; + else if ($event.mode === ButtonMode.DoublePress) message = `Double press `; + let entityName = Helper.getEntityName(this.activity?.options?.included_entities?.find(item => item.entity_id === $event.command.entity_id)); + if (!entityName ||entityName === "") entityName = $event.command.entity_id; + const commandName = Helper.getCommandName($event.command, this.configEntityCommands); + message += `${entityName} ${commandName}`; + if ($event.error) + message = `${message} : ${$event.error}`; + this.onMessage.emit({severity: $event.severity, detail: message}); + } + handleMessage($event: {button: ButtonMapping, mode: ButtonMode, severity: "success" | "error", error?: string}) { let message = "Short press"; - if ($event.mode === ButtonMode.ShortPress) message = `Short press ${$event.button.short_press?.entity_id} ${$event.button.short_press?.cmd_id}`; - else if ($event.mode === ButtonMode.LongPress) message = `Long press ${$event.button.long_press?.entity_id} ${$event.button.long_press?.cmd_id}`; - else if ($event.mode === ButtonMode.DoublePress) message = `Double press ${$event.button.double_press?.entity_id} ${$event.button.double_press?.cmd_id}`; + const command = $event.mode == ButtonMode.ShortPress ? $event.button.short_press : + ($event.mode == ButtonMode.LongPress ? $event.button.long_press : $event.button.double_press); + if (!command) return; + let entityName = Helper.getEntityName(this.activity?.options?.included_entities?.find(item => item.entity_id === command.entity_id)); + if (!entityName ||entityName === "") entityName = command.entity_id; + const commandName = Helper.getCommandName(command, this.configEntityCommands); + + if ($event.mode === ButtonMode.ShortPress) message = `Short press ${entityName} ${commandName}`; + else if ($event.mode === ButtonMode.LongPress) message = `Long press ${entityName} ${commandName}`; + else if ($event.mode === ButtonMode.DoublePress) message = `Double press ${entityName} ${commandName}`; if ($event.error) message = `${message} : ${$event.error}`; this.onMessage.emit({severity: $event.severity, detail: message}); } + + getVolumeEntity(): Observable + { + if (!this.remote) return of(undefined); + const button = this.activity?.options?.button_mapping?.find(button => button.button === 'VOLUME_UP'); + const volumeEntity = button?.short_press?.entity_id; + if (!volumeEntity) return of(undefined); + const entity = this.activity?.options?.included_entities?.find(entity => entity.entity_id === volumeEntity); + if (!entity || entity.entity_type !== 'media_player') return of(undefined); + return this.server.getRemotetEntity(this.remote, entity.entity_id!).pipe(map(entity => { + if (!Helper.checkFeature(entity, "volume")) return undefined; + return {...entity, new_state: {attributes: {...entity.attributes}, features: entity.features}} as MediaEntityState; + })) + } + + updateVolume(volume: number, entity_id: string) { + if (!this.remote) return; + console.debug("Volume update", volume, entity_id); + let name = Helper.getEntityName(this.volumeEntity); + if (!name || name === "") name = entity_id; + this.server.executeRemotetCommand(this.remote, {entity_id, + cmd_id:"media_player.volume", params: {"volume": volume}}).subscribe({ + next: (results) => { + this.onMessage.emit({severity: "success", detail: `Volume set ${name} : ${volume}%`}); + }, + error: err => this.onMessage.emit({severity: "error", + detail: `Error volume set ${name} : ${volume}% (${err.toString()})`}) + }); + } + + protected readonly Math = Math; } diff --git a/src/app/activity-viewer/actiivty-media-entity/activity-media-entity.component.html b/src/app/activity-viewer/actiivty-media-entity/activity-media-entity.component.html index bbfa51b..a60cd76 100644 --- a/src/app/activity-viewer/actiivty-media-entity/activity-media-entity.component.html +++ b/src/app/activity-viewer/actiivty-media-entity/activity-media-entity.component.html @@ -4,13 +4,13 @@ [alt]="mediaEntityState?.new_state?.attributes?.media_title" />
- +
- +
- +
= new EventEmitter(); constructor(protected remoteWebsocketService: RemoteWebsocketService, private server:ServerService, private cdr: ChangeDetectorRef) { @@ -116,7 +120,14 @@ export class ActivityMediaEntityComponent implements AfterViewInit { const body = {entity_id: mediaEntity.entity_id, cmd_id:"media_player.seek", params: {"media_position": newPosition}}; console.debug("Seek", body); - this.server.executeRemotetCommand(this.remote, body).subscribe( - {error: err => console.error("Error updting position", err)}); + + this.server.executeRemotetCommand(this.remote, body).subscribe({next: results => { + this.onSelectButton.emit({command: body, mode: ButtonMode.ShortPress, severity: "success"}); + }, error: (err: HttpErrorResponse) => { + console.error("Error command", err); + this.onSelectButton.emit({command: body, mode: ButtonMode.ShortPress, severity: "error"}); + this.onSelectButton.emit({command:body, mode: ButtonMode.ShortPress, severity: "error", + error: `Error updating position : ${err.error.name} (${err.status} ${err.statusText})`}); + }}); } } diff --git a/src/app/activity-viewer/activity-grid/activity-grid.component.html b/src/app/activity-viewer/activity-grid/activity-grid.component.html index a2e5d4c..2ca27e2 100644 --- a/src/app/activity-viewer/activity-grid/activity-grid.component.html +++ b/src/app/activity-viewer/activity-grid/activity-grid.component.html @@ -10,7 +10,6 @@ (itemClicked)="gridItemClicked($event)" [selectionMode]="selectionMode" [runMode]="runMode" class="grid-item">
- @@ -25,13 +24,14 @@ [style]="Helper.getStyle(getEntityName((item.command | as : Command)?.entity_id!))"> - +
Media player - +
diff --git a/src/app/activity-viewer/activity-grid/activity-grid.component.ts b/src/app/activity-viewer/activity-grid/activity-grid.component.ts index e45c4d2..e25baac 100644 --- a/src/app/activity-viewer/activity-grid/activity-grid.component.ts +++ b/src/app/activity-viewer/activity-grid/activity-grid.component.ts @@ -2,9 +2,16 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, - Component, HostListener, - Input, Pipe, PipeTransform, QueryList, - ViewChild, ViewChildren, + Component, + EventEmitter, + HostListener, + Input, + Output, + Pipe, + PipeTransform, + QueryList, + ViewChild, + ViewChildren, ViewEncapsulation } from '@angular/core'; import {Helper} from "../../helper"; @@ -12,21 +19,14 @@ import {ActivityGridItemComponent} from "../activity-grid-item/activity-grid-ite import {ChipModule} from "primeng/chip"; import {NgForOf, NgIf} from "@angular/common"; import {TagModule} from "primeng/tag"; -import { - Activity, - ActivityPageCommand, - Command, - EntityCommand, - Remote, - ScreenLayout, - UIPage -} from "../../interfaces"; +import {Activity, ActivityPageCommand, Command, EntityCommand, Remote, ScreenLayout, UIPage} from "../../interfaces"; import {UiCommandEditorComponent} from "../../activity-editor/ui-command-editor/ui-command-editor.component"; import {HttpErrorResponse} from "@angular/common/http"; import {ServerService} from "../../server.service"; import {MessageService} from "primeng/api"; import {ToastModule} from "primeng/toast"; import {ActivityMediaEntityComponent} from "../actiivty-media-entity/activity-media-entity.component"; +import {ButtonMode} from "../activity-buttons/activity-buttons.component"; @Pipe({name: 'as', standalone: true, pure: true}) export class AsPipe implements PipeTransform { @@ -85,6 +85,8 @@ export class ActivityGridComponent implements AfterViewInit { selection: ActivityGridItemComponent[] = []; @ViewChild("commandeditor", {static: false}) commandeditor: UiCommandEditorComponent | undefined; @ViewChildren(ActivityGridItemComponent) gridButtons:QueryList | undefined; + @Output() onSelectButton: EventEmitter<{command: Command, mode: ButtonMode, severity: "success" | "error", + error?: string}> = new EventEmitter(); configEntityCommands: EntityCommand[] | undefined; public Command!: Command; @@ -229,17 +231,17 @@ export class ActivityGridComponent implements AfterViewInit { executeCommand(command: Command) { if (!this.remote) return; this.server.executeRemotetCommand(this.remote, command).subscribe({next: results => { - this.messageService.add({key: "activityGrid", summary: "Command executed", - severity: "success", detail: `Results : ${results.code} : ${results.message}`}); + this.onSelectButton.emit({command, mode: ButtonMode.ShortPress, severity: "success"}); }, error: (err: HttpErrorResponse) => { console.error("Error command", err); - this.messageService.add({key: "activityGrid", summary: "Error executing command", - severity: "error", detail: `Results : ${err.error.name} (${err.status} ${err.statusText})`}); + this.onSelectButton.emit({command, mode: ButtonMode.ShortPress, severity: "error", + error: `${err.error.name} (${err.status} ${err.statusText})`}); }}); this.cdr.detectChanges(); } + addGridItem($event: ActivityGridItemComponent) { const position = {x: $event.item.location.x, y: $event.item.location.y, width: $event.item.size.width, height: $event.item.size.height}; diff --git a/src/app/controls/scrolling-text/scrolling-text.component.ts b/src/app/controls/scrolling-text/scrolling-text.component.ts index bcbb728..1ad8b99 100644 --- a/src/app/controls/scrolling-text/scrolling-text.component.ts +++ b/src/app/controls/scrolling-text/scrolling-text.component.ts @@ -35,12 +35,13 @@ export class ScrollingTextComponent implements AfterViewInit { @Input("textStyle") set _textStyle(textStyle: string | undefined) { this.textStyle = textStyle; + this.cdr.detectChanges(); this.updateClass(); } @ViewChild("textContainer", {static: false}) textContainer: ElementRef | undefined; @ViewChild("textContent", {static: false}) textContent: ElementRef | undefined; - constructor(private cdr:ChangeDetectorRef) { + constructor(private elementRef: ElementRef, private cdr:ChangeDetectorRef) { } ngAfterViewInit(): void { diff --git a/src/app/controls/slider/slider.component.css b/src/app/controls/slider/slider.component.css index 11743e1..1ed5c6e 100644 --- a/src/app/controls/slider/slider.component.css +++ b/src/app/controls/slider/slider.component.css @@ -71,3 +71,10 @@ cursor: pointer; } +.secondary-state .p-slider .p-slider-range { + background: rgba(202, 202, 202, 0.8); +} + +.secondary-state .p-progressbar .p-progressbar-value{ + background: rgba(202, 202, 202, 0.8); +} diff --git a/src/app/controls/slider/slider.component.html b/src/app/controls/slider/slider.component.html index f91fece..d9d1363 100644 --- a/src/app/controls/slider/slider.component.html +++ b/src/app/controls/slider/slider.component.html @@ -1,13 +1,14 @@
- +
{{textValue}}
{{max}}
- + {{textValue}}
diff --git a/src/app/controls/slider/slider.component.ts b/src/app/controls/slider/slider.component.ts index 6eb0ac6..d6746d2 100644 --- a/src/app/controls/slider/slider.component.ts +++ b/src/app/controls/slider/slider.component.ts @@ -47,6 +47,7 @@ export class SliderComponent implements OnInit { private sliderSubject: Subject = new Subject(); private subscription = new Subscription(); @Input() max: string | undefined; + @Input() secondaryState = false; constructor( private cdr:ChangeDetectorRef) { } diff --git a/src/app/helper.ts b/src/app/helper.ts index f76236e..3c31710 100644 --- a/src/app/helper.ts +++ b/src/app/helper.ts @@ -9,6 +9,7 @@ import { Remote, ActivityPageCommand, OrphanEntity, CommandSequence, EntityCommand, EntityCommandParameter } from "./interfaces"; +import {MediaEntityState} from "./remote-websocket.service"; export class Helper { @@ -560,4 +561,20 @@ export class Helper static setScale(scale: number) { localStorage.setItem("scale", scale.toString()); } + + static checkFeature(entity: Entity|undefined, feature: string | string[]): boolean + { + const features = (Array.isArray(feature)) ? feature as string[] : [feature]; + return entity?.features?.find(item => features.includes(item)) !== undefined; + } + + static getNumber(number: number) { + if (isNaN(number)) return 0; + return Math.round(number); + } + + static isMuted(volumeEntity: MediaEntityState | undefined) { + if (!volumeEntity?.new_state?.attributes) return false; + return !!volumeEntity?.new_state?.attributes.muted; + } } diff --git a/src/app/remote-widget/remote-websocket.service.spec.ts b/src/app/remote-websocket.service.spec.ts similarity index 100% rename from src/app/remote-widget/remote-websocket.service.spec.ts rename to src/app/remote-websocket.service.spec.ts diff --git a/src/app/remote-widget/remote-websocket.service.ts b/src/app/remote-websocket.service.ts similarity index 97% rename from src/app/remote-widget/remote-websocket.service.ts rename to src/app/remote-websocket.service.ts index 94327d5..0db3c45 100644 --- a/src/app/remote-widget/remote-websocket.service.ts +++ b/src/app/remote-websocket.service.ts @@ -1,9 +1,9 @@ import {Injectable, OnDestroy, OnInit} from '@angular/core'; -import {ServerService} from "../server.service"; -import {EventMessage, RequestMessage, ResponseMessage, WebsocketService} from "../websocket.service"; -import {BatteryState, Entity, Remote} from "../interfaces"; +import {ServerService} from "./server.service"; +import {EventMessage, RequestMessage, ResponseMessage, WebsocketService} from "./websocket.service"; +import {BatteryState, Entity, Remote} from "./interfaces"; import {BehaviorSubject, map, Observable, Observer, share, Subject, Subscription, timer} from "rxjs"; -import {Helper} from "../helper"; +import {Helper} from "./helper"; export interface MediaEntityState { @@ -28,6 +28,7 @@ export interface MediaEntityState source_list?: string[]; sound_mode?: string; sound_mode_list?: string[]; + muted?: boolean; } } } @@ -114,6 +115,11 @@ export class RemoteWebsocketService implements OnDestroy { return this.websocketService.connectionStatus$; } + public isRemoteConnected(): boolean + { + return this.websocketService.isRemoteConnected(); + } + initWidget() { if (!this._mediaEntity && this._mediaEntities?.length > 0) { diff --git a/src/app/remote-widget/remote-widget.component.html b/src/app/remote-widget/remote-widget.component.html index 0fdf5c2..df10728 100644 --- a/src/app/remote-widget/remote-widget.component.html +++ b/src/app/remote-widget/remote-widget.component.html @@ -26,27 +26,25 @@ -
-
- - -
- - - {{remoteWebsocketService.getEntityName(mediaEntity)}} - - - {{remoteWebsocketService.getEntityName(item)}} - - -
-
-
-
-
+
+ + +
+ + + {{remoteWebsocketService.getEntityName(mediaEntity)}} + + + {{remoteWebsocketService.getEntityName(item)}} + + +
+
+
diff --git a/src/app/remote-widget/remote-widget.component.ts b/src/app/remote-widget/remote-widget.component.ts index 73336c5..a94e5ef 100644 --- a/src/app/remote-widget/remote-widget.component.ts +++ b/src/app/remote-widget/remote-widget.component.ts @@ -16,7 +16,7 @@ import {ProgressBarModule} from "primeng/progressbar"; import {ScrollingTextComponent} from "../controls/scrolling-text/scrolling-text.component"; import {DropdownModule} from "primeng/dropdown"; import {FormsModule} from "@angular/forms"; -import {MediaEntityState, RemoteState, RemoteWebsocketService} from "./remote-websocket.service"; +import {MediaEntityState, RemoteState, RemoteWebsocketService} from "../remote-websocket.service"; import {Activity, Remote, RemoteData} from "../interfaces"; import {MediaEntityComponent} from "../active-entities/media-entity/media-entity.component"; import {DropdownOverComponent} from "../controls/dropdown-over/dropdown-over.component"; @@ -47,6 +47,9 @@ import {DropdownOverComponent} from "../controls/dropdown-over/dropdown-over.com }) export class RemoteWidgetComponent implements OnInit { @Input() visible = true; + @Input() scale = 0.8; + protected readonly Math = Math; + minimized = false; remoteState: RemoteState | undefined; mediaEntity: MediaEntityState | undefined; @@ -57,6 +60,8 @@ export class RemoteWidgetComponent implements OnInit { constructor(private server:ServerService, protected remoteWebsocketService: RemoteWebsocketService, private cdr:ChangeDetectorRef) { } ngOnInit(): void { + const scale = localStorage.getItem("scale"); + if (scale) this.scale = Number.parseFloat(scale); this.remoteWebsocketService.onRemoteStateChange().subscribe(remoteState => { this.remoteState = remoteState; this.cdr.detectChanges(); @@ -114,6 +119,4 @@ export class RemoteWidgetComponent implements OnInit { changedMediaEntity($event: any) { this.remoteWebsocketService.mediaEntity = this.mediaEntity; } - - protected readonly Math = Math; } diff --git a/src/app/server.service.ts b/src/app/server.service.ts index 4510a27..6f55526 100644 --- a/src/app/server.service.ts +++ b/src/app/server.service.ts @@ -358,6 +358,21 @@ export class ServerService { })) } + wakeRemote(remote: Remote, broadcast?: string): Observable + { + const body: any = {} + if (broadcast) { + let httpOptions = {params: new HttpParams({fromObject: {broadcast}})}; + return this.http.post(`/api/remote/${remote.address}/wake`, body, httpOptions).pipe(map(results => { + return results; + })) + } + else + return this.http.post(`/api/remote/${remote.address}/wake`, body).pipe(map(results => { + return results; + })) + } + unregisterRemote(remote: Remote): Observable { return this.http.delete('/api/config/remote/'+remote.address).pipe(map(results => { diff --git a/src/app/websocket.service.ts b/src/app/websocket.service.ts index 34e42a3..89517d1 100644 --- a/src/app/websocket.service.ts +++ b/src/app/websocket.service.ts @@ -2,7 +2,7 @@ import {Injectable, OnDestroy, OnInit} from '@angular/core'; import {ServerService} from "./server.service"; import {Remote} from "./interfaces"; import {webSocket, WebSocketSubject} from "rxjs/webSocket"; -import {BehaviorSubject, Observable, retry, Subject, timer} from "rxjs"; +import {BehaviorSubject, delay, Observable, retry, Subject, timer} from "rxjs"; import {distinctUntilChanged, filter, map, skip, take, tap} from "rxjs/operators"; export interface Message @@ -76,6 +76,11 @@ export class WebsocketService implements OnDestroy { return this.remoteChanged$; } + isRemoteConnected() : boolean + { + return this.status$.getValue(); + } + getMessageEvent(): Subject { @@ -139,6 +144,7 @@ export class WebsocketService implements OnDestroy { console.debug("Websocket disconnect, reconnecting..."); this.initWebsocket(); }), + delay(this.reconnecInterval) ).subscribe(); }