Initial skeletal Frigate lovelace card.
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
resources:
|
resources:
|
||||||
- url: http://127.0.0.1:5000/boilerplate-card.js
|
- url: http://127.0.0.1:5000/frigate-card.js
|
||||||
type: module
|
type: module
|
||||||
views:
|
views:
|
||||||
- cards:
|
- cards:
|
||||||
- type: custom:boilerplate-card
|
- type: custom:frigate-card
|
||||||
name: Boilerplate Card Development
|
name: Frigate Card Development
|
||||||
entity: sun.sun
|
|
||||||
test_gui: true
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
---
|
||||||
name: 'Build'
|
name: 'Build'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
schedule:
|
||||||
- master
|
- cron: "17 6 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
# Build
|
# Build
|
||||||
- name: Build the file
|
- name: Build the file
|
||||||
run: |
|
run: |
|
||||||
cd /home/runner/work/boilerplate-card/boilerplate-card
|
cd /home/runner/work/frigate-card/frigate-card
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
file: /home/runner/work/boilerplate-card/boilerplate-card/dist/boilerplate-card.js
|
file: /home/runner/work/frigate-card/frigate-card/dist/frigate-card.js
|
||||||
asset_name: boilerplate-card.js
|
asset_name: frigate-card.js
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
@@ -1,119 +1,3 @@
|
|||||||
# Boilerplate Card by [@iantrich](https://www.github.com/iantrich)
|
# Frigate Lovelace Card
|
||||||
|
|
||||||
A community driven boilerplate of best practices for Home Assistant Lovelace custom cards
|
A pre-alpha Frigate Lovelace card.
|
||||||
|
|
||||||
[![GitHub Release][releases-shield]][releases]
|
|
||||||
[![License][license-shield]](LICENSE.md)
|
|
||||||
[](https://github.com/custom-components/hacs)
|
|
||||||
|
|
||||||
![Project Maintenance][maintenance-shield]
|
|
||||||
[![GitHub Activity][commits-shield]][commits]
|
|
||||||
|
|
||||||
[![Discord][discord-shield]][discord]
|
|
||||||
[![Community Forum][forum-shield]][forum]
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Hey dude! Help me out for a couple of :beers: or a :coffee:!
|
|
||||||
|
|
||||||
[](https://www.buymeacoffee.com/zJtVxUAgH)
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
| Name | Type | Requirement | Description | Default |
|
|
||||||
| ----------------- | ------- | ------------ | ------------------------------------------- | ------------------- |
|
|
||||||
| type | string | **Required** | `custom:boilerplate-card` |
|
|
||||||
| name | string | **Optional** | Card name | `Boilerplate` |
|
|
||||||
| show_error | boolean | **Optional** | Show what an error looks like for the card | `false` |
|
|
||||||
| show_warning | boolean | **Optional** | Show what a warning looks like for the card | `false` |
|
|
||||||
| entity | string | **Optional** | Home Assistant entity ID. | `none` |
|
|
||||||
| tap_action | object | **Optional** | Action to take on tap | `action: more-info` |
|
|
||||||
| hold_action | object | **Optional** | Action to take on hold | `none` |
|
|
||||||
| double_tap_action | object | **Optional** | Action to take on double tap | `none` |
|
|
||||||
|
|
||||||
## Action Options
|
|
||||||
|
|
||||||
| Name | Type | Requirement | Description | Default |
|
|
||||||
| --------------- | ------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
|
||||||
| action | string | **Required** | Action to perform (more-info, toggle, call-service, navigate url, none) | `more-info` |
|
|
||||||
| navigation_path | string | **Optional** | Path to navigate to (e.g. /lovelace/0/) when action defined as navigate | `none` |
|
|
||||||
| url | string | **Optional** | URL to open on click when action is url. The URL will open in a new tab | `none` |
|
|
||||||
| service | string | **Optional** | Service to call (e.g. media_player.media_play_pause) when action defined as call-service | `none` |
|
|
||||||
| service_data | object | **Optional** | Service data to include (e.g. entity_id: media_player.bedroom) when action defined as call-service | `none` |
|
|
||||||
| haptic | string | **Optional** | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) _success, warning, failure, light, medium, heavy, selection_ | `none` |
|
|
||||||
| repeat | number | **Optional** | How often to repeat the `hold_action` in milliseconds. | `non` |
|
|
||||||
|
|
||||||
## Starting a new card from boilerplate-card
|
|
||||||
|
|
||||||
### Step 1
|
|
||||||
|
|
||||||
Clone this repository
|
|
||||||
|
|
||||||
### Step 2
|
|
||||||
|
|
||||||
Install necessary modules (verified to work in node 8.x)
|
|
||||||
`yarn install` or `npm install`
|
|
||||||
|
|
||||||
### Step 3
|
|
||||||
|
|
||||||
Do a test lint & build on the project. You can see available scripts in the package.json
|
|
||||||
`npm run build`
|
|
||||||
|
|
||||||
### Step 4
|
|
||||||
|
|
||||||
Search the repository for all instances of "TODO" and handle the changes/suggestions
|
|
||||||
|
|
||||||
### Step 5
|
|
||||||
|
|
||||||
Customize to suit your needs and contribute it back to the community
|
|
||||||
|
|
||||||
## Starting a new card from boilerplate-card with [devcontainer][devcontainer]
|
|
||||||
|
|
||||||
Note: this is available only in vscode ensure you have the [Remote Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension installed.
|
|
||||||
|
|
||||||
1. Fork and clone the repository.
|
|
||||||
2. Open a [devcontainer][devcontainer] terminal and run `npm start` when it's ready.
|
|
||||||
3. The compiled `.js` file will be accessible on
|
|
||||||
`http://127.0.0.1:5000/boilerplate-card.js`.
|
|
||||||
4. On a running Home Assistant installation add this to your Lovelace
|
|
||||||
`resources:`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- url: 'http://127.0.0.1:5000/boilerplate-card.js'
|
|
||||||
type: module
|
|
||||||
```
|
|
||||||
|
|
||||||
_Change "127.0.0.1" to the IP of your development machine._
|
|
||||||
|
|
||||||
### Bonus
|
|
||||||
|
|
||||||
If you need a fresh test instance you can install a fresh Home Assistant instance inside the devcontainer as well.
|
|
||||||
|
|
||||||
1. Run the command `container start`.
|
|
||||||
2. Home Assistant will install and will eventually be running on port `9123`
|
|
||||||
|
|
||||||
## [Troubleshooting](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins)
|
|
||||||
|
|
||||||
NB This will not work with node 9.x if you see the following errors try installing node 8.10.0
|
|
||||||
|
|
||||||
```yarn install
|
|
||||||
yarn install v1.3.2
|
|
||||||
[1/4] 🔍 Resolving packages...
|
|
||||||
warning rollup-plugin-commonjs@10.1.0: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.
|
|
||||||
[2/4] 🚚 Fetching packages...
|
|
||||||
error @typescript-eslint/eslint-plugin@2.6.0: The engine "node" is incompatible with this module. Expected version "^8.10.0 || ^10.13.0 || >=11.10.1".
|
|
||||||
error Found incompatible module
|
|
||||||
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
|
|
||||||
```
|
|
||||||
|
|
||||||
[commits-shield]: https://img.shields.io/github/commit-activity/y/custom-cards/boilerplate-card.svg?style=for-the-badge
|
|
||||||
[commits]: https://github.com/custom-cards/boilerplate-card/commits/master
|
|
||||||
[devcontainer]: https://code.visualstudio.com/docs/remote/containers
|
|
||||||
[discord]: https://discord.gg/5e9yvq
|
|
||||||
[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge
|
|
||||||
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge
|
|
||||||
[forum]: https://community.home-assistant.io/c/projects/frontend
|
|
||||||
[license-shield]: https://img.shields.io/github/license/custom-cards/boilerplate-card.svg?style=for-the-badge
|
|
||||||
[maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg?style=for-the-badge
|
|
||||||
[releases-shield]: https://img.shields.io/github/release/custom-cards/boilerplate-card.svg?style=for-the-badge
|
|
||||||
[releases]: https://github.com/custom-cards/boilerplate-card/releases
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "Boilerplate Card",
|
"name": "Frigate Card",
|
||||||
"render_readme": true,
|
"render_readme": true,
|
||||||
"filename": "boilerplate-card.js"
|
"filename": "frigate-card.js"
|
||||||
}
|
}
|
||||||
+30
-27
@@ -1,47 +1,50 @@
|
|||||||
{
|
{
|
||||||
"name": "boilerplate-card",
|
"name": "frigate-hass-card",
|
||||||
"version": "1.3.2",
|
"version": "0.0.1",
|
||||||
"description": "Lovelace boilerplate-card",
|
"description": "Frigate Lovelace Card for Home Assistant",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"frigate",
|
||||||
"home-assistant",
|
"home-assistant",
|
||||||
"homeassistant",
|
"homeassistant",
|
||||||
"hass",
|
"hass",
|
||||||
"automation",
|
"lovelace"
|
||||||
"lovelace",
|
|
||||||
"custom-cards"
|
|
||||||
],
|
],
|
||||||
"module": "boilerplate-card.js",
|
"module": "frigate-card.js",
|
||||||
"repository": "git@github.com:custom_cards/boilerplate-card.git",
|
"repository": "git@github.com:dermotduffy/frigate-hass-card.git",
|
||||||
"author": "Ian Richardson <iantrich@gmail.com>",
|
"author": "Dermot Duffy <dermot.duffy@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"custom-card-helpers": "^1.6.6",
|
"@material/image-list": "^12.0.0",
|
||||||
"home-assistant-js-websocket": "^4.5.0",
|
"custom-card-helpers": "^1.7.1",
|
||||||
"lit-element": "^2.4.0",
|
"home-assistant-js-websocket": "^5.11.1",
|
||||||
"lit-html": "^1.3.0"
|
"lit-element": "^2.5.1",
|
||||||
|
"lit-html": "^1.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.3",
|
"@babel/core": "^7.15.0",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||||
"@babel/plugin-proposal-decorators": "^7.12.1",
|
"@babel/plugin-proposal-decorators": "^7.14.5",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||||
"@typescript-eslint/parser": "^2.34.0",
|
"@typescript-eslint/parser": "^4.29.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"eslint-config-prettier": "^6.15.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.23.4",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"prettier": "^1.19.1",
|
"npm-check-updates": "^11.8.3",
|
||||||
"rollup": "^1.32.1",
|
"prettier": "^2.3.2",
|
||||||
|
"rollup": "^2.56.0",
|
||||||
|
"rollup-plugin-styles": "^3.14.1",
|
||||||
"rollup-plugin-babel": "^4.4.0",
|
"rollup-plugin-babel": "^4.4.0",
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-serve": "^1.1.0",
|
"rollup-plugin-serve": "^1.1.0",
|
||||||
"rollup-plugin-terser": "^5.3.1",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"rollup-plugin-typescript2": "^0.24.3",
|
"rollup-plugin-typescript2": "^0.30.0",
|
||||||
"rollup-plugin-uglify": "^6.0.4",
|
"rollup-plugin-uglify": "^6.0.4",
|
||||||
"typescript": "^3.9.7"
|
"sass": "^1.37.5",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "rollup -c rollup.config.dev.js --watch",
|
"start": "rollup -c rollup.config.dev.js --watch",
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import resolve from "rollup-plugin-node-resolve";
|
|
||||||
import typescript from "rollup-plugin-typescript2";
|
|
||||||
import babel from "rollup-plugin-babel";
|
|
||||||
import serve from "rollup-plugin-serve";
|
|
||||||
import { terser } from "rollup-plugin-terser";
|
|
||||||
import json from '@rollup/plugin-json';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: ["src/boilerplate-card.ts"],
|
|
||||||
output: {
|
|
||||||
dir: "./dist",
|
|
||||||
format: "es",
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
resolve(),
|
|
||||||
typescript(),
|
|
||||||
json(),
|
|
||||||
babel({
|
|
||||||
exclude: "node_modules/**",
|
|
||||||
}),
|
|
||||||
terser(),
|
|
||||||
serve({
|
|
||||||
contentBase: "./dist",
|
|
||||||
host: "0.0.0.0",
|
|
||||||
port: 5000,
|
|
||||||
allowCrossOrigin: true,
|
|
||||||
headers: {
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
+8
-1
@@ -5,6 +5,7 @@ import babel from 'rollup-plugin-babel';
|
|||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from 'rollup-plugin-terser';
|
||||||
import serve from 'rollup-plugin-serve';
|
import serve from 'rollup-plugin-serve';
|
||||||
import json from '@rollup/plugin-json';
|
import json from '@rollup/plugin-json';
|
||||||
|
import styles from "rollup-plugin-styles";
|
||||||
|
|
||||||
const dev = process.env.ROLLUP_WATCH;
|
const dev = process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
@@ -19,6 +20,12 @@ const serveopts = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
|
styles({
|
||||||
|
modules: false,
|
||||||
|
sass: {
|
||||||
|
includePaths: ["./node_modules/"]
|
||||||
|
}
|
||||||
|
}),
|
||||||
nodeResolve({}),
|
nodeResolve({}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
typescript(),
|
typescript(),
|
||||||
@@ -32,7 +39,7 @@ const plugins = [
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
input: 'src/boilerplate-card.ts',
|
input: 'src/frigate-card.ts',
|
||||||
output: {
|
output: {
|
||||||
dir: 'dist',
|
dir: 'dist',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
|
|||||||
@@ -161,16 +161,15 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO You need to replace all instances of "action-handler-boilerplate" with "action-handler-<your card name>"
|
customElements.define('action-handler-frigate-card', ActionHandler);
|
||||||
customElements.define('action-handler-boilerplate', ActionHandler);
|
|
||||||
|
|
||||||
const getActionHandler = (): ActionHandler => {
|
const getActionHandler = (): ActionHandler => {
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
if (body.querySelector('action-handler-boilerplate')) {
|
if (body.querySelector('action-handler-frigate-card')) {
|
||||||
return body.querySelector('action-handler-boilerplate') as ActionHandler;
|
return body.querySelector('action-handler-frigate-card') as ActionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionhandler = document.createElement('action-handler-boilerplate');
|
const actionhandler = document.createElement('action-handler-frigate-card');
|
||||||
body.appendChild(actionhandler);
|
body.appendChild(actionhandler);
|
||||||
|
|
||||||
return actionhandler as ActionHandler;
|
return actionhandler as ActionHandler;
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import {
|
|
||||||
LitElement,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
property,
|
|
||||||
CSSResult,
|
|
||||||
TemplateResult,
|
|
||||||
css,
|
|
||||||
PropertyValues,
|
|
||||||
internalProperty,
|
|
||||||
} from 'lit-element';
|
|
||||||
import {
|
|
||||||
HomeAssistant,
|
|
||||||
hasConfigOrEntityChanged,
|
|
||||||
hasAction,
|
|
||||||
ActionHandlerEvent,
|
|
||||||
handleAction,
|
|
||||||
LovelaceCardEditor,
|
|
||||||
getLovelace,
|
|
||||||
} from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types
|
|
||||||
|
|
||||||
import './editor';
|
|
||||||
|
|
||||||
import type { BoilerplateCardConfig } from './types';
|
|
||||||
import { actionHandler } from './action-handler-directive';
|
|
||||||
import { CARD_VERSION } from './const';
|
|
||||||
import { localize } from './localize/localize';
|
|
||||||
|
|
||||||
/* eslint no-console: 0 */
|
|
||||||
console.info(
|
|
||||||
`%c BOILERPLATE-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
|
|
||||||
'color: orange; font-weight: bold; background: black',
|
|
||||||
'color: white; font-weight: bold; background: dimgray',
|
|
||||||
);
|
|
||||||
|
|
||||||
// This puts your card into the UI card picker dialog
|
|
||||||
(window as any).customCards = (window as any).customCards || [];
|
|
||||||
(window as any).customCards.push({
|
|
||||||
type: 'boilerplate-card',
|
|
||||||
name: 'Boilerplate Card',
|
|
||||||
description: 'A template custom card for you to create something awesome',
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO Name your custom element
|
|
||||||
@customElement('boilerplate-card')
|
|
||||||
export class BoilerplateCard extends LitElement {
|
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
|
||||||
return document.createElement('boilerplate-card-editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getStubConfig(): object {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Add any properities that should cause your element to re-render here
|
|
||||||
// https://lit-element.polymer-project.org/guide/properties
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
@internalProperty() private config!: BoilerplateCardConfig;
|
|
||||||
|
|
||||||
// https://lit-element.polymer-project.org/guide/properties#accessors-custom
|
|
||||||
public setConfig(config: BoilerplateCardConfig): void {
|
|
||||||
// TODO Check for required fields and that they are of the proper format
|
|
||||||
if (!config) {
|
|
||||||
throw new Error(localize('common.invalid_configuration'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.test_gui) {
|
|
||||||
getLovelace().setEditMode(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.config = {
|
|
||||||
name: 'Boilerplate',
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://lit-element.polymer-project.org/guide/lifecycle#shouldupdate
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
|
||||||
if (!this.config) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasConfigOrEntityChanged(this, changedProps, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://lit-element.polymer-project.org/guide/templates
|
|
||||||
protected render(): TemplateResult | void {
|
|
||||||
// TODO Check for stateObj or other necessary things and render a warning if missing
|
|
||||||
if (this.config.show_warning) {
|
|
||||||
return this._showWarning(localize('common.show_warning'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config.show_error) {
|
|
||||||
return this._showError(localize('common.show_error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-card
|
|
||||||
.header=${this.config.name}
|
|
||||||
@action=${this._handleAction}
|
|
||||||
.actionHandler=${actionHandler({
|
|
||||||
hasHold: hasAction(this.config.hold_action),
|
|
||||||
hasDoubleClick: hasAction(this.config.double_tap_action),
|
|
||||||
})}
|
|
||||||
tabindex="0"
|
|
||||||
.label=${`Boilerplate: ${this.config.entity || 'No Entity Defined'}`}
|
|
||||||
></ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleAction(ev: ActionHandlerEvent): void {
|
|
||||||
if (this.hass && this.config && ev.detail.action) {
|
|
||||||
handleAction(this, this.hass, this.config, ev.detail.action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showWarning(warning: string): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<hui-warning>${warning}</hui-warning>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showError(error: string): TemplateResult {
|
|
||||||
const errorCard = document.createElement('hui-error-card');
|
|
||||||
errorCard.setConfig({
|
|
||||||
type: 'error',
|
|
||||||
error,
|
|
||||||
origConfig: this.config,
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${errorCard}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://lit-element.polymer-project.org/guide/styles
|
|
||||||
static get styles(): CSSResult {
|
|
||||||
return css``;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const CARD_VERSION = '1.3.2';
|
export const CARD_VERSION = '0.0.1';
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module '*.scss';
|
||||||
+8
-9
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/camelcase */
|
|
||||||
import {
|
import {
|
||||||
LitElement,
|
LitElement,
|
||||||
html,
|
html,
|
||||||
@@ -8,11 +7,11 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
css,
|
css,
|
||||||
internalProperty,
|
state,
|
||||||
} from 'lit-element';
|
} from 'lit-element';
|
||||||
import { HomeAssistant, fireEvent, LovelaceCardEditor, ActionConfig } from 'custom-card-helpers';
|
import { HomeAssistant, fireEvent, LovelaceCardEditor, ActionConfig } from 'custom-card-helpers';
|
||||||
|
|
||||||
import { BoilerplateCardConfig } from './types';
|
import { FrigateCardConfig } from './types';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
required: {
|
required: {
|
||||||
@@ -55,15 +54,15 @@ const options = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement('boilerplate-card-editor')
|
@customElement('frigate-card-editor')
|
||||||
export class BoilerplateCardEditor extends LitElement implements LovelaceCardEditor {
|
export class FrigateCardEditor extends LitElement implements LovelaceCardEditor {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
@internalProperty() private _config?: BoilerplateCardConfig;
|
@state() private _config?: FrigateCardConfig;
|
||||||
@internalProperty() private _toggle?: boolean;
|
@state() private _toggle?: boolean;
|
||||||
@internalProperty() private _helpers?: any;
|
@state() private _helpers?: any;
|
||||||
private _initialized = false;
|
private _initialized = false;
|
||||||
|
|
||||||
public setConfig(config: BoilerplateCardConfig): void {
|
public setConfig(config: FrigateCardConfig): void {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
|
|
||||||
this.loadCardHelpers();
|
this.loadCardHelpers();
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
@use "@material/image-list/mdc-image-list";
|
||||||
|
@use "@material/image-list";
|
||||||
|
|
||||||
|
.frigate-card-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-gallery {
|
||||||
|
overflow: auto;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
left: 55px;
|
||||||
|
right: 0px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
-ms-overflow-style: none; // Hide scrollbar: IE and Edge
|
||||||
|
scrollbar-width: none; // Hide scrollbar: Firefox
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-exception {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
.frigate-card-gallery::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-image-list {
|
||||||
|
@include image-list.standard-columns(4);
|
||||||
|
@include image-list.shape-radius(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-image-list-icon-overlay {
|
||||||
|
position: absolute;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-image-list-highlight {
|
||||||
|
// Put border inside.
|
||||||
|
box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-navbar {
|
||||||
|
position: absolute;
|
||||||
|
width: 50px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frigate-card-statusbar {
|
||||||
|
@extend .frigate-card-navbar;
|
||||||
|
right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-icon-button.button {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 50%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.visibile {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
// TODO: Can I use ajv (https://ajv.js.org/guide/typescript.html) to verify
|
||||||
|
// event return matches the TS interface?
|
||||||
|
|
||||||
|
// TODO Does each event contain thumbnail?
|
||||||
|
|
||||||
|
// TODO comments per method.
|
||||||
|
|
||||||
|
// TODO: Stop the live view video when I hide it.
|
||||||
|
|
||||||
|
// TODO Check for HA state presence and validity before using it, otherwise warn.
|
||||||
|
|
||||||
|
// TODO Add material tooltips
|
||||||
|
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
property,
|
||||||
|
CSSResult,
|
||||||
|
TemplateResult,
|
||||||
|
PropertyValues,
|
||||||
|
state,
|
||||||
|
unsafeCSS,
|
||||||
|
} from 'lit-element';
|
||||||
|
|
||||||
|
import { until } from 'lit-html/directives/until.js';
|
||||||
|
import {
|
||||||
|
HomeAssistant,
|
||||||
|
ActionHandlerEvent,
|
||||||
|
handleAction,
|
||||||
|
LovelaceCardEditor,
|
||||||
|
getLovelace,
|
||||||
|
} from 'custom-card-helpers';
|
||||||
|
|
||||||
|
import './editor';
|
||||||
|
|
||||||
|
import style from './frigate-card.scss'
|
||||||
|
|
||||||
|
import type { FrigateCardConfig, FrigateEvent, GetEventsParameters } from './types';
|
||||||
|
import { actionHandler } from './action-handler-directive';
|
||||||
|
import { CARD_VERSION } from './const';
|
||||||
|
import { localize } from './localize/localize';
|
||||||
|
|
||||||
|
/* eslint no-console: 0 */
|
||||||
|
console.info(
|
||||||
|
`%c FRIGATE-HASS-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
|
||||||
|
'color: pink; font-weight: bold; background: black',
|
||||||
|
'color: white; font-weight: bold; background: dimgray',
|
||||||
|
);
|
||||||
|
|
||||||
|
// This puts your card into the UI card picker dialog
|
||||||
|
(window as any).customCards = (window as any).customCards || [];
|
||||||
|
(window as any).customCards.push({
|
||||||
|
type: 'frigate-card',
|
||||||
|
name: 'Frigate Card',
|
||||||
|
description: 'A lovelace card for use with Frigate',
|
||||||
|
});
|
||||||
|
|
||||||
|
enum FrigateCardView {
|
||||||
|
LIVE, // Show the live camera.
|
||||||
|
CLIP, // Show a clip video.
|
||||||
|
CLIPS, // Show the clips gallery.
|
||||||
|
SNAPSHOT, // Show a snapshot.
|
||||||
|
SNAPSHOTS, // Show the snapshots gallery.
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card')
|
||||||
|
export class FrigateCard extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._viewMode = FrigateCardView.LIVE;
|
||||||
|
this._viewEvent = null;
|
||||||
|
this._interactionTimerID = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
|
return document.createElement('frigate-card-editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getStubConfig(): Record<string, string> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public config!: FrigateCardConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected _viewMode: FrigateCardView;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected _viewEvent: FrigateEvent | null;
|
||||||
|
|
||||||
|
protected _interactionTimerID: number | null;
|
||||||
|
|
||||||
|
public setConfig(inputConfig: FrigateCardConfig): void {
|
||||||
|
if (!inputConfig) {
|
||||||
|
throw new Error(localize('common.invalid_configuration:'));
|
||||||
|
}
|
||||||
|
// inputConfig is not extensible, need to make a copy to allow
|
||||||
|
// modifications.
|
||||||
|
const cardConfig = Object.assign({
|
||||||
|
name: 'Frigate'
|
||||||
|
}, inputConfig);
|
||||||
|
|
||||||
|
if (cardConfig.test_gui) {
|
||||||
|
getLovelace().setEditMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cardConfig.frigate_url) {
|
||||||
|
throw new Error(localize('common.invalid_configuration_missing') + ": frigate_url");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cardConfig.frigate_camera_name) {
|
||||||
|
// No camera name specified, so just assume it's the same as the entity name.
|
||||||
|
if (cardConfig.camera_entity.includes(".")) {
|
||||||
|
cardConfig.frigate_camera_name = cardConfig.camera_entity.split('.', 2)[1]
|
||||||
|
} else {
|
||||||
|
throw new Error(localize('common.invalid_configuration_missing') + ": camera");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardConfig.timeout_ms) {
|
||||||
|
if (isNaN(Number(cardConfig.timeout_ms))) {
|
||||||
|
throw new Error(localize('common.invalid_configuration') + ": timeout_ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardConfig.default_view) {
|
||||||
|
if (!["live", "clips", "clip", "snapshots", "snapshot"].includes(cardConfig.default_view)) {
|
||||||
|
throw new Error(localize('common.invalid_configuration') + ": default_view");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = cardConfig;
|
||||||
|
this._setViewModeToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _setViewModeToDefault(): void {
|
||||||
|
if (this.config.default_view == "live") {
|
||||||
|
this._viewMode = FrigateCardView.LIVE;
|
||||||
|
} else if (this.config.default_view == "clips") {
|
||||||
|
this._viewMode = FrigateCardView.CLIPS;
|
||||||
|
} else if (this.config.default_view == "clip") {
|
||||||
|
this._viewMode = FrigateCardView.CLIP;
|
||||||
|
} else if (this.config.default_view == "snapshots") {
|
||||||
|
this._viewMode = FrigateCardView.SNAPSHOTS;
|
||||||
|
} else if (this.config.default_view == "snapshot") {
|
||||||
|
this._viewMode = FrigateCardView.SNAPSHOT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// == RTC experimentation ==
|
||||||
|
// const div = document.createElement("div");
|
||||||
|
// const webrtcElement = customElements.get('webrtc-camera');
|
||||||
|
// const webrtc = new webrtcElement();
|
||||||
|
// webrtc.setConfig({ "entity": "camera.landing_rtsp" });
|
||||||
|
// webrtc.hass = this.hass;
|
||||||
|
// div.appendChild(webrtc);
|
||||||
|
// this.renderRoot.appendChild(div);
|
||||||
|
// ==
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
if (!this.config || !this.hass) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraEntity = this.config.camera_entity;
|
||||||
|
const motionEntity = this.config.motion_entity;
|
||||||
|
|
||||||
|
if (!cameraEntity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has('config')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
||||||
|
|
||||||
|
if (oldHass) {
|
||||||
|
if (oldHass.states[cameraEntity] !== this.hass.states[cameraEntity]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (motionEntity && oldHass.states[motionEntity] !== this.hass.states[motionEntity]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _getEvents({
|
||||||
|
has_clip = false,
|
||||||
|
has_snapshot = false,
|
||||||
|
limit = 100,
|
||||||
|
}: GetEventsParameters): Promise<FrigateEvent[]> {
|
||||||
|
let url = `${this.config.frigate_url}/api/events?camera=${this.config.frigate_camera_name}`;
|
||||||
|
if (has_clip) {
|
||||||
|
url += `&has_clip=1`
|
||||||
|
}
|
||||||
|
if (has_snapshot) {
|
||||||
|
url += `&has_snapshot=1`
|
||||||
|
}
|
||||||
|
if (limit > 0) {
|
||||||
|
url += `&limit=${limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.label) {
|
||||||
|
url += `&label=${this.config.label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
} else {
|
||||||
|
// TODO: Catch when json decoding fails.
|
||||||
|
throw new Error(`Frigate API request failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _renderEvents() : Promise<TemplateResult> {
|
||||||
|
const want_clips = this._viewMode == FrigateCardView.CLIPS;
|
||||||
|
|
||||||
|
const events = await this._getEvents({
|
||||||
|
has_clip: want_clips,
|
||||||
|
has_snapshot: !want_clips,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return html`
|
||||||
|
<div class="frigate-card-exception">
|
||||||
|
<ha-icon
|
||||||
|
icon="${want_clips ? "mdi:filmstrip-off" : "mdi:camera-off"}"
|
||||||
|
></ha-icon>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ul class= "mdc-image-list frigate-card-image-list">
|
||||||
|
${events.map(event => html`
|
||||||
|
<li class="mdc-image-list__item">
|
||||||
|
<div class="mdc-image-list__image-aspect-container">
|
||||||
|
<img
|
||||||
|
class="mdc-image-list__image"
|
||||||
|
src="data:image/png;base64,${event.thumbnail}"
|
||||||
|
@click=${() => {
|
||||||
|
this._viewEvent = event;
|
||||||
|
this._viewMode = want_clips ?
|
||||||
|
FrigateCardView.CLIP : FrigateCardView.SNAPSHOT
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</li>`)}
|
||||||
|
</ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _renderProgressIndicator(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="frigate-card-exception">
|
||||||
|
<ha-circular-progress
|
||||||
|
active="true"
|
||||||
|
size="large"
|
||||||
|
></ha-circular-progress>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _renderNavigationBar(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="frigate-card-navbar" >
|
||||||
|
<ha-icon-button
|
||||||
|
class="button"
|
||||||
|
icon="mdi:cctv"
|
||||||
|
@click=${() => this._viewMode = FrigateCardView.LIVE}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
class="button"
|
||||||
|
icon = "mdi:filmstrip"
|
||||||
|
@click=${() => this._viewMode = FrigateCardView.CLIPS}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
class="button"
|
||||||
|
icon = "mdi:camera"
|
||||||
|
@click=${() => this._viewMode = FrigateCardView.SNAPSHOTS}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _renderClipPlayer(): Promise<TemplateResult> {
|
||||||
|
let event: FrigateEvent;
|
||||||
|
if (!this._viewEvent) {
|
||||||
|
const events = await this._getEvents({
|
||||||
|
has_clip: true,
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
if (!events.length) {
|
||||||
|
return html`
|
||||||
|
<div class="frigate-card-exception">
|
||||||
|
<ha-icon
|
||||||
|
icon="mdi:camera-off"
|
||||||
|
></ha-icon>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
event = events[0];
|
||||||
|
} else {
|
||||||
|
event = this._viewEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.config.frigate_url}/clips/` +
|
||||||
|
`${event.camera}-${event.id}.mp4`;
|
||||||
|
return html`
|
||||||
|
<video class="frigate-card-viewer" autoplay controls>
|
||||||
|
<source src="${url}" type="video/mp4">
|
||||||
|
</video>`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _renderSnapshotViewer(): TemplateResult {
|
||||||
|
if (!this._viewEvent) {
|
||||||
|
return html``
|
||||||
|
}
|
||||||
|
const url = `${this.config.frigate_url}/clips/` +
|
||||||
|
`${this._viewEvent.camera}-${this._viewEvent.id}.jpg`;
|
||||||
|
return html`<img class="frigate-card-viewer" src="${url}">`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _renderStatusBar(): TemplateResult {
|
||||||
|
if (!this.config.motion_entity || !(this.config.motion_entity in this.hass.states)) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
const icon = this.hass.states[this.config.motion_entity].state == "on" ?
|
||||||
|
"mdi:motion-sensor" : "mdi:walk"
|
||||||
|
return html`
|
||||||
|
<div class="frigate-card-statusbar ${
|
||||||
|
this._viewMode == FrigateCardView.LIVE ? 'visible' : 'invisible'}
|
||||||
|
">
|
||||||
|
<ha-icon-button
|
||||||
|
class="button"
|
||||||
|
icon="${icon}"
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _renderLiveViewer(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-camera-stream
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.hass.states[this.config.camera_entity]}
|
||||||
|
.controls=${true}
|
||||||
|
.muted=${true}
|
||||||
|
class=${this._viewMode == FrigateCardView.LIVE ? 'visible' : 'invisible'}
|
||||||
|
>
|
||||||
|
</ha-camera-stream>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _interactionHandler(): void {
|
||||||
|
if (!this.config.timeout_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._interactionTimerID) {
|
||||||
|
window.clearTimeout(this._interactionTimerID);
|
||||||
|
}
|
||||||
|
this._interactionTimerID = window.setTimeout(() => {
|
||||||
|
this._interactionTimerID = null;
|
||||||
|
this._setViewModeToDefault();
|
||||||
|
}, this.config.timeout_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (this.config.show_warning) {
|
||||||
|
return this._showWarning(localize('common.show_warning'));
|
||||||
|
}
|
||||||
|
if (this.config.show_error) {
|
||||||
|
return this._showError(localize('common.show_error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add latest snapshot fetch functionality.
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="frigate-card-container"
|
||||||
|
@click=${this._interactionHandler}
|
||||||
|
>
|
||||||
|
${this._renderNavigationBar()}
|
||||||
|
${this._viewMode == FrigateCardView.CLIPS ?
|
||||||
|
html`<div class="frigate-card-gallery">
|
||||||
|
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||||
|
</div>` : ``
|
||||||
|
}
|
||||||
|
${this._viewMode == FrigateCardView.SNAPSHOTS ?
|
||||||
|
html`<div class="frigate-card-gallery">
|
||||||
|
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||||
|
</div>` : ``
|
||||||
|
}
|
||||||
|
${this._viewMode == FrigateCardView.CLIP ?
|
||||||
|
html`<div class="frigate-card-viewer">
|
||||||
|
${until(this._renderClipPlayer(), this._renderProgressIndicator())}
|
||||||
|
</div>` : ``
|
||||||
|
}
|
||||||
|
${this._viewMode == FrigateCardView.SNAPSHOT ?
|
||||||
|
this._renderSnapshotViewer() : ``
|
||||||
|
}
|
||||||
|
${this._renderStatusBar()}
|
||||||
|
${this._renderLiveViewer()}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// private _handleAction(ev: ActionHandlerEvent): void {
|
||||||
|
// if (this.hass && this.config && ev.detail.action) {
|
||||||
|
// handleAction(this, this.hass, this.config, ev.detail.action);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
private _showWarning(warning: string): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<hui-warning> ${warning} </hui-warning>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showError(error: string): TemplateResult {
|
||||||
|
const errorCard = document.createElement('hui-error-card');
|
||||||
|
errorCard.setConfig({
|
||||||
|
type: 'error',
|
||||||
|
error,
|
||||||
|
origConfig: this.config,
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${errorCard}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
// CSS is compiled from frigate-card.scss, so this is safe.
|
||||||
|
return unsafeCSS(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCardSize(): number {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"invalid_configuration": "Invalid configuration",
|
"invalid_configuration": "Invalid configuration",
|
||||||
|
"invalid_configuration_missing": "Missing parameter in configuration",
|
||||||
"show_warning": "Show Warning",
|
"show_warning": "Show Warning",
|
||||||
"show_error": "Show Error"
|
"show_error": "Show Error"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"version": "Versjon",
|
|
||||||
"invalid_configuration": "Ikke gyldig konfiguration",
|
|
||||||
"show_warning": "Vis advarsel"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import * as en from './languages/en.json';
|
import * as en from './languages/en.json';
|
||||||
import * as nb from './languages/nb.json';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const languages: any = {
|
const languages: any = {
|
||||||
en: en,
|
en: en,
|
||||||
nb: nb,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function localize(string: string, search = '', replace = ''): string {
|
export function localize(string: string, search = '', replace = ''): string {
|
||||||
|
|||||||
+30
-5
@@ -1,21 +1,46 @@
|
|||||||
import { ActionConfig, LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from 'custom-card-helpers';
|
import { ActionConfig, LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from 'custom-card-helpers';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'boilerplate-card-editor': LovelaceCardEditor;
|
'frigate-card-editor': LovelaceCardEditor;
|
||||||
'hui-error-card': LovelaceCard;
|
'hui-error-card': LovelaceCard;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Add your configuration elements here for type-checking
|
export interface FrigateCardConfig extends LovelaceCardConfig {
|
||||||
export interface BoilerplateCardConfig extends LovelaceCardConfig {
|
|
||||||
type: string;
|
type: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
camera_entity: string;
|
||||||
|
motion_entity: string | null;
|
||||||
|
frigate_url: string;
|
||||||
|
frigate_camera_name?: string | null;
|
||||||
|
default_view: string | null;
|
||||||
|
timeout_ms?: number | null;
|
||||||
|
|
||||||
show_warning?: boolean;
|
show_warning?: boolean;
|
||||||
show_error?: boolean;
|
show_error?: boolean;
|
||||||
test_gui?: boolean;
|
test_gui?: boolean;
|
||||||
entity?: string;
|
|
||||||
tap_action?: ActionConfig;
|
tap_action?: ActionConfig;
|
||||||
hold_action?: ActionConfig;
|
hold_action?: ActionConfig;
|
||||||
double_tap_action?: ActionConfig;
|
double_tap_action?: ActionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FrigateEvent {
|
||||||
|
camera: string;
|
||||||
|
end_time: number;
|
||||||
|
false_positive: boolean;
|
||||||
|
has_clip: boolean;
|
||||||
|
has_snapshot: boolean;
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
start_time: number;
|
||||||
|
thumbnail: string;
|
||||||
|
top_score: number;
|
||||||
|
zones: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetEventsParameters {
|
||||||
|
has_clip?: boolean;
|
||||||
|
has_snapshot?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user