feat: Add UI optional UI locking when microphone is hot (#2484)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:12 -07:00
committed by dermotduffy
parent 9a763db1f3
commit be7e7d79dc
45 changed files with 922 additions and 53 deletions
+9 -1
View File
@@ -70,6 +70,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
@property({ attribute: false })
public microphoneState?: MicrophoneState;
@property({ attribute: false })
public locked?: boolean;
@property({ attribute: false })
public viewFilterCameraID?: string;
@@ -247,6 +250,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
.zoom=${!this._isGesturesPTZActive(view, cameraID)}
.forceSelected=${isSelectedSlide}
.locked=${this.locked}
@advanced-camera-card:zoom:change=${(
ev: CustomEvent<ZoomSettingsObserved>,
) =>
@@ -327,6 +331,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
.label=${neighbor?.metadata?.title ?? ''}
.icon=${neighbor?.metadata?.icon}
?disabled=${!neighbor}
?locked=${!!this.locked}
@click=${(ev) => {
this._setViewCameraID(neighbor?.id);
stopEventFromActivatingCardWideActions(ev);
@@ -360,7 +365,10 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
: view.context?.ptzControls?.enabled;
const dragEnabled =
hasMultipleCameras && this.liveConfig?.draggable && !gesturesPTZActive;
hasMultipleCameras &&
this.liveConfig?.draggable &&
!gesturesPTZActive &&
!this.locked;
return html`
<advanced-camera-card-carousel
+4
View File
@@ -39,6 +39,9 @@ export class AdvancedCameraCardLiveGrid extends LitElement {
@property({ attribute: false })
public microphoneState?: MicrophoneState;
@property({ attribute: false })
public locked?: boolean;
@property({ attribute: false, hasChanged: contentsChanged })
public triggeredCameraIDs?: Set<string>;
@@ -63,6 +66,7 @@ export class AdvancedCameraCardLiveGrid extends LitElement {
.cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager}
.microphoneState=${this.microphoneState}
.locked=${this.locked}
?triggered=${triggeredCameraID &&
!!this.triggeredCameraIDs?.has(triggeredCameraID)}
>
+4
View File
@@ -43,6 +43,9 @@ export class AdvancedCameraCardLive extends LitElement {
@property({ attribute: false })
public microphoneState?: MicrophoneState;
@property({ attribute: false })
public locked?: boolean;
@property({ attribute: false, hasChanged: contentsChanged })
public triggeredCameraIDs?: Set<string>;
@@ -93,6 +96,7 @@ export class AdvancedCameraCardLive extends LitElement {
.cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager}
.microphoneState=${this.microphoneState}
.locked=${this.locked}
.triggeredCameraIDs=${this.triggeredCameraIDs}
>
</advanced-camera-card-live-grid>
+11 -2
View File
@@ -66,6 +66,12 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
@property({ attribute: false })
public forceSelected = false;
// When true the UI lock is active and the native video controls must be
// suppressed: those controls expose pause/play/cast/etc. directly on the
// media element.
@property({ attribute: false })
public locked?: boolean;
private _mediaLoadedInfoSinkController = new MediaLoadedInfoSinkController(this, {
getTargetID: () => this.targetID ?? null,
});
@@ -181,14 +187,17 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
return result;
}
// Builtin (native) video controls require all three conditions:
// Builtin (native) video controls require all four conditions:
// - controls.builtin: user config enables native controls.
// - zoom: Whether digital zoom/panning is allowed (this will be false when a
// 'gesture' type PTZ control is active).
// - !_zoomed: the user has not actually digital zoomed in (when zoomed, we
// want to hide the controls).
// - !locked: the UI lock is not active.
private _getEffectiveBuiltinControls(): boolean {
return !!this.liveConfig?.controls.builtin && this.zoom && !this._zoomed;
return (
!!this.liveConfig?.controls.builtin && this.zoom && !this._zoomed && !this.locked
);
}
private _renderContainer(template: TemplateResult): TemplateResult {
+19 -6
View File
@@ -2,12 +2,13 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit
import { customElement, property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../action-handler-directive.js';
import type { LockManagerEpoch } from '../card-controller/lock/types';
import { MenuController } from '../components-lib/menu-controller.js';
import { MenuItem } from '../config/schema/elements/custom/menu/types.js';
import { MenuConfig } from '../config/schema/menu.js';
import type { MenuItem } from '../config/schema/elements/custom/menu/types.js';
import type { MenuConfig } from '../config/schema/menu.js';
import { getEntityTitle } from '../ha/get-entity-title.js';
import { EntityRegistryManager } from '../ha/registry/entity/types.js';
import { HomeAssistant } from '../ha/types.js';
import type { EntityRegistryManager } from '../ha/registry/entity/types.js';
import type { HomeAssistant } from '../ha/types.js';
import menuStyle from '../scss/menu.scss';
import { hasAction } from '../utils/action.js';
import './icon.js';
@@ -24,6 +25,9 @@ export class AdvancedCameraCardMenu extends LitElement {
@property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false })
public lockManagerEpoch?: LockManagerEpoch;
set menuConfig(menuConfig: MenuConfig) {
this._controller.setMenuConfig(menuConfig);
}
@@ -36,6 +40,12 @@ export class AdvancedCameraCardMenu extends LitElement {
this._controller.setExpanded(expanded);
}
protected willUpdate(changedProps: Map<string, unknown>): void {
if (changedProps.has('lockManagerEpoch')) {
this._controller.setLockManagerEpoch(this.lockManagerEpoch);
}
}
public toggleMenu(): void {
this._controller.toggleExpanded();
}
@@ -49,7 +59,8 @@ export class AdvancedCameraCardMenu extends LitElement {
return html` <advanced-camera-card-submenu-button
.hass=${this.hass}
.submenu=${button}
@action=${(ev) => this._controller.handleAction(ev)}
.lockManagerEpoch=${this.lockManagerEpoch}
@action=${(ev) => this._controller.handleAction(ev, button)}
>
</advanced-camera-card-submenu-button>`;
} else if (button.type === 'custom:advanced-camera-card-menu-submenu-select') {
@@ -57,7 +68,8 @@ export class AdvancedCameraCardMenu extends LitElement {
.hass=${this.hass}
.submenuSelect=${button}
.entityRegistryManager=${this.entityRegistryManager}
@action=${(ev) => this._controller.handleAction(ev)}
.lockManagerEpoch=${this.lockManagerEpoch}
@action=${(ev) => this._controller.handleAction(ev, button)}
>
</advanced-camera-card-submenu-select-button>`;
}
@@ -75,6 +87,7 @@ export class AdvancedCameraCardMenu extends LitElement {
hasDoubleClick: hasAction(button.double_tap_action),
})}
.label=${title ?? ''}
?disabled=${this._controller.shouldButtonBeInert(button)}
@action=${(ev) => this._controller.handleAction(ev, button)}
>
<advanced-camera-card-icon
+10 -3
View File
@@ -2,8 +2,9 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
import { customElement, property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../../action-handler-directive.js';
import type { LockManagerEpoch } from '../../card-controller/lock/types';
import { getEntityTitle } from '../../ha/get-entity-title.js';
import { HomeAssistant } from '../../ha/types.js';
import type { HomeAssistant } from '../../ha/types.js';
import submenuStyle from '../../scss/submenu.scss';
import {
hasAction,
@@ -11,7 +12,7 @@ import {
} from '../../utils/action.js';
import { contentsChanged } from '../../utils/basic.js';
import '../icon.js';
import { SubmenuInteraction, SubmenuItem } from './types.js';
import type { SubmenuInteraction, SubmenuItem } from './types.js';
@customElement('advanced-camera-card-submenu')
export class AdvancedCameraCardSubmenu extends LitElement {
@@ -21,6 +22,9 @@ export class AdvancedCameraCardSubmenu extends LitElement {
@property({ attribute: false, hasChanged: contentsChanged })
public items?: SubmenuItem[];
@property({ attribute: false })
public lockManagerEpoch?: LockManagerEpoch;
private _renderItem(item: SubmenuItem): TemplateResult | void {
if (!this.hass) {
return;
@@ -28,7 +32,10 @@ export class AdvancedCameraCardSubmenu extends LitElement {
const title = item.title ?? getEntityTitle(this.hass, item.entity);
const style = styleMap(item.style || {});
const disabled = item.enabled === false;
const inert =
!!this.lockManagerEpoch?.locked &&
this.lockManagerEpoch.manager.areAllActionsBlocked(item);
const disabled = item.enabled === false || inert;
return html`
<ha-dropdown-item
+17 -6
View File
@@ -8,17 +8,19 @@ import {
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { InternalIcon } from '../../config/schema/common/icon.js';
import { MenuSubmenuSelect } from '../../config/schema/elements/custom/menu/submenu-select.js';
import { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
import { actionHandler } from '../../action-handler-directive.js';
import type { LockManagerEpoch } from '../../card-controller/lock/types';
import type { InternalIcon } from '../../config/schema/common/icon.js';
import type { MenuSubmenuSelect } from '../../config/schema/elements/custom/menu/submenu-select.js';
import type { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
import { computeDomain } from '../../ha/compute-domain.js';
import { getEntityStateTranslation } from '../../ha/entity-state-translation.js';
import { getEntityTitle } from '../../ha/get-entity-title.js';
import { isHassDifferent } from '../../ha/is-hass-different.js';
import { EntityRegistryManager } from '../../ha/registry/entity/types.js';
import { HomeAssistant } from '../../ha/types.js';
import type { EntityRegistryManager } from '../../ha/registry/entity/types.js';
import type { HomeAssistant } from '../../ha/types.js';
import menuButtonStyle from '../../scss/menu-button.scss';
import { createSelectOptionAction } from '../../utils/action.js';
import { createSelectOptionAction, hasAction } from '../../utils/action.js';
import '../icon.js';
import './index.js';
@@ -33,6 +35,9 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
@property({ attribute: false })
public entityRegistryManager?: EntityRegistryManager;
@property({ attribute: false })
public lockManagerEpoch?: LockManagerEpoch;
@state()
private _optionTitles?: Record<string, string>;
@@ -129,6 +134,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
return html` <advanced-camera-card-submenu
.hass=${this.hass}
.items=${this._generatedSubmenuItems}
.lockManagerEpoch=${this.lockManagerEpoch}
>
<ha-icon-button style="${style}" .label=${title || ''}>
<advanced-camera-card-icon
@@ -137,6 +143,11 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
title=${title || ''}
.hass=${this.hass}
.icon=${this._generatedIcon}
.actionHandler=${actionHandler({
allowPropagation: true,
hasHold: hasAction(this.submenuSelect.hold_action),
hasDoubleClick: hasAction(this.submenuSelect.double_tap_action),
})}
></advanced-camera-card-icon>
</ha-icon-button>
</advanced-camera-card-submenu>`;
+7 -2
View File
@@ -2,8 +2,9 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
import { customElement, property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../../action-handler-directive.js';
import { MenuSubmenu } from '../../config/schema/elements/custom/menu/submenu.js';
import { HomeAssistant } from '../../ha/types.js';
import type { LockManagerEpoch } from '../../card-controller/lock/types';
import type { MenuSubmenu } from '../../config/schema/elements/custom/menu/submenu.js';
import type { HomeAssistant } from '../../ha/types.js';
import menuButtonStyle from '../../scss/menu-button.scss';
import { hasAction } from '../../utils/action.js';
import '../icon.js';
@@ -17,6 +18,9 @@ export class AdvancedCameraCardSubmenuButton extends LitElement {
@property({ attribute: false })
public submenu?: MenuSubmenu;
@property({ attribute: false })
public lockManagerEpoch?: LockManagerEpoch;
protected render(): TemplateResult {
if (!this.submenu) {
return html``;
@@ -26,6 +30,7 @@ export class AdvancedCameraCardSubmenuButton extends LitElement {
return html` <advanced-camera-card-submenu
.hass=${this.hass}
.items=${this.submenu?.items}
.lockManagerEpoch=${this.lockManagerEpoch}
>
<ha-icon-button style="${style}" .label=${this.submenu.title || ''}>
<advanced-camera-card-icon
+5
View File
@@ -51,6 +51,9 @@ export class AdvancedCameraCardSurround extends LitElement {
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
@property({ attribute: false })
public locked?: boolean;
/**
* Determine if a drawer is being used.
*/
@@ -102,6 +105,7 @@ export class AdvancedCameraCardSurround extends LitElement {
.foldersManager=${this.foldersManager}
.selected=${view.queryResults?.getSelectedIndex() ?? undefined}
.cardWideConfig=${this.cardWideConfig}
.locked=${this.locked}
>
</advanced-camera-card-thumbnail-carousel>`
: ''}
@@ -123,6 +127,7 @@ export class AdvancedCameraCardSurround extends LitElement {
.conditionStateManager=${this.conditionStateManager}
.viewItemManager=${this.viewItemManager}
.cardWideConfig=${this.cardWideConfig}
.locked=${this.locked}
>
</advanced-camera-card-timeline-core>`
: ''}
+3
View File
@@ -65,6 +65,9 @@ export class AdvancedCameraCardThumbnailCarousel extends LitElement {
@property({ attribute: false })
public fadeThumbnails = false;
@property({ type: Boolean, reflect: true })
public locked?: boolean;
private _thumbnails: TemplateResult[] = [];
private _builder: UnifiedQueryBuilder | null = null;
+3
View File
@@ -137,6 +137,9 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
@property({ attribute: false })
public itemClickAction?: TimelineItemClickAction;
@property({ type: Boolean, reflect: true })
public locked?: boolean;
private _refDatePicker: Ref<AdvancedCameraCardDatePicker> = createRef();
private _refTimeline: Ref<HTMLElement> = createRef();
private _controller: TimelineController = new TimelineController(this);
+5
View File
@@ -67,6 +67,9 @@ export class AdvancedCameraCardViews extends LitElement {
@property({ attribute: false })
public microphoneState?: MicrophoneState;
@property({ attribute: false })
public locked?: boolean;
@property({ attribute: false, hasChanged: contentsChanged })
public triggeredCameraIDs?: Set<string>;
@@ -163,6 +166,7 @@ export class AdvancedCameraCardViews extends LitElement {
.conditionStateManager=${this.conditionStateManager}
.viewItemManager=${this.viewItemManager}
.cardWideConfig=${this.cardWideConfig}
.locked=${this.locked}
>
${!this.hide && view?.is('image')
? html` <advanced-camera-card-image
@@ -236,6 +240,7 @@ export class AdvancedCameraCardViews extends LitElement {
.cardWideConfig=${this.cardWideConfig}
.microphoneManager=${this.microphoneManager}
.microphoneState=${this.microphoneState}
.locked=${this.locked}
.triggeredCameraIDs=${this.triggeredCameraIDs}
class="${classMap(liveClasses)}"
>