0 && ratio.h > 0
+ ? `${((100 * ratio.h) / ratio.w).toFixed(2)}%`
+ : "",
+ })}
+ class=${classMap({
+ ratio: Boolean(ratio && ratio.w > 0 && ratio.h > 0),
+ })}
+ >
+

+
+
+ `;
+ }
+
+ protected updated(changedProps: PropertyValues): void {
+ if (changedProps.has("cameraImage")) {
+ this._updateCameraImageSrc();
+ this._startUpdateCameraInterval();
+ return;
+ }
+ }
+
+ private _startUpdateCameraInterval() {
+ this._stopUpdateCameraInterval();
+ if (this.cameraImage && this._attached) {
+ this._cameraUpdater = window.setInterval(
+ () => this._updateCameraImageSrc(),
+ UPDATE_INTERVAL
+ );
+ }
+ }
+
+ private _stopUpdateCameraInterval() {
+ if (this._cameraUpdater) {
+ clearInterval(this._cameraUpdater);
+ }
+ }
+
+ private _onImageError() {
+ this._loadError = true;
+ }
+
+ private async _onImageLoad() {
+ this._loadError = false;
+ await this.updateComplete;
+ this._lastImageHeight = this._image.offsetHeight;
+ }
+
+ private async _updateCameraImageSrc() {
+ if (!this.hass || !this.cameraImage) {
+ return;
+ }
+ if (this._cameraImageSrc) {
+ URL.revokeObjectURL(this._cameraImageSrc);
+ this._cameraImageSrc = undefined;
+ }
+ try {
+ const { content_type: contentType, content } = await fetchThumbnail(
+ this.hass,
+ this.cameraImage
+ );
+ this._cameraImageSrc = URL.createObjectURL(
+ b64toBlob(content, contentType)
+ );
+ this._onImageLoad();
+ } catch (err) {
+ this._onImageError();
+ }
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ img {
+ display: block;
+ height: auto;
+ transition: filter 0.2s linear;
+ width: 100%;
+ }
+
+ .ratio {
+ position: relative;
+ width: 100%;
+ height: 0;
+ }
+
+ .ratio img,
+ .ratio div {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ #brokenImage {
+ background: grey url("/static/images/image-broken.svg") center/36px
+ no-repeat;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-image": HuiImage;
+ }
+}
+
+customElements.define("hui-image", HuiImage);