Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create switch and light entities from UI #112

Merged
merged 75 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
ae08dac
initial commit - switch
farmio Nov 20, 2023
dc2e373
fix DataTable column template argument
farmio Nov 20, 2023
73023f6
entity category
farmio Nov 21, 2023
2196016
Add entities_router and edit
farmio Dec 9, 2023
459c950
cleanup and style
farmio Dec 11, 2023
c1f6cc4
catch edit unknown entity
farmio Dec 11, 2023
c798c55
fix top margin of content in create entity
farmio Dec 11, 2023
a03ee03
confirm entity deletion
farmio Dec 11, 2023
2b6b964
Assign devices to entities; create new devices
farmio Dec 18, 2023
6a1e045
Always have "add_new" in device picker; first item
farmio Dec 18, 2023
26555df
Use device identifier instead of id outside of picker
farmio Dec 18, 2023
0555768
fix config key and device edit initial value
farmio Dec 18, 2023
b37824e
Clear Device input when "Add new device" was canceled
farmio Dec 19, 2023
621e984
Use `device_info` instead of `device_id` to avoid confusion with `id`
farmio Dec 21, 2023
3d5dd74
Outsource entity settings card
farmio Dec 21, 2023
82ff3d1
Filter entities view for device_id in URL params
farmio Dec 21, 2023
32549b6
Show device name instead of entity name in entities view
farmio Dec 21, 2023
8e5e40b
Request schema options before creating an entity
farmio Dec 22, 2023
38065fc
Show entity settings after creation
farmio Dec 23, 2023
f868eea
remove device_class in favour of more-info-dialog
farmio Dec 23, 2023
3243f81
Add knx-group-address-selector component
farmio Dec 27, 2023
e680579
Rename ga keys; hide passive address
farmio Dec 27, 2023
8a254ce
Animate passive address toggle
farmio Dec 27, 2023
a18f095
Add missing KNX-Project interface definitions
farmio Dec 29, 2023
d7cf4cb
Add device tree panel to entities_create
farmio Jan 1, 2024
f36c7c6
Fix entity type selection card position
farmio Jan 2, 2024
6bf72b8
drag&drop
farmio Jan 13, 2024
abe9bb8
wrap li content in div, clean up styles
farmio Jan 14, 2024
e3330e3
add manufacturer to devices
farmio Jan 14, 2024
1621f5c
max-width; nicer icons
farmio Jan 14, 2024
979c7d5
dpt and flag infos
farmio Jan 14, 2024
8f14bc2
back to device selection
farmio Jan 14, 2024
1e49c86
knx colors
farmio Jan 15, 2024
9e4761d
back item design
farmio Jan 15, 2024
7dc9cc3
hover effect for clickable items
farmio Jan 16, 2024
9585b50
fix DPT validation
farmio Jan 16, 2024
c4e30d2
drop zone indicator
farmio Jan 17, 2024
891c33c
only valid GAs dragable
farmio Jan 17, 2024
200b1cb
filter invalid DPT
farmio Jan 18, 2024
04a2f3e
revert only valid GAs dragable
farmio Jan 18, 2024
011dfde
use drag indicator icons
farmio Jan 18, 2024
94eeb7d
margin for ha-selector
farmio Jan 18, 2024
30c9f83
Receive validation result
farmio Feb 4, 2024
9843e7a
Use ha-selector-select directly
farmio Feb 6, 2024
4849144
highlight when dragOver
farmio Feb 7, 2024
cb71157
reset invalid state of textfield if set before drag
farmio Feb 7, 2024
73c7a79
flex the box
farmio Feb 8, 2024
4f8e6c1
Prevent width-flickering when drilling into devices
farmio Feb 9, 2024
c0d4975
build forms from hardcoded entity schema
farmio Feb 16, 2024
f70277c
same for edit entity
farmio Feb 16, 2024
6f33a1a
use selectors directly; cleanup
farmio Feb 16, 2024
ccf78f1
Receive validation result when editing entities
farmio Feb 16, 2024
8dead5c
Validate again when config may be corrected
farmio Feb 19, 2024
c5c6ec5
Move validation error alert and scroll to it
farmio Feb 20, 2024
44f0202
sort some css
farmio Feb 20, 2024
b5729d6
allow all HA selectors in schema
farmio Feb 20, 2024
4c23857
Use EntityData in configure-entity; add platform in entities_create/edit
farmio Feb 20, 2024
f058e9f
Apply default value from schema to selector
farmio Feb 23, 2024
f257928
Reuse GASchemaOption type
farmio Feb 23, 2024
cb9404a
Edit unknown entity
farmio Feb 23, 2024
8c480a4
combine entities_create and entities_edit into one component
farmio Feb 24, 2024
7b92e22
Add optional DPT selector for GA-selector
farmio Feb 25, 2024
9bdfca6
GASchema: use one of validDPTs or dptSelect
farmio Feb 26, 2024
2b2eac2
Clear DPTSelect disabled when no GA is selected
farmio Feb 26, 2024
b4db888
Add custom knx-dpt-selector supporting validation error message
farmio Feb 26, 2024
617373f
don't disable dptSelect for unknown GAs
farmio Feb 27, 2024
93be305
Prettier validation error
farmio Feb 29, 2024
bf178dd
Add selector for different groups of selectors
farmio Feb 29, 2024
8c6a6c5
Configurable ga-selector combo-box label
farmio Feb 29, 2024
074bba1
move knx config to extra key, show entity errors at fields
farmio Mar 9, 2024
06c8c19
minor typing enhancment
farmio Mar 9, 2024
668a915
Edit entities base on entity_id instead of unique_id
farmio May 10, 2024
6a115f3
add Light Schema
farmio May 17, 2024
16f7f4b
remove SchemaOptions
farmio May 17, 2024
a5089e4
Remove false from state updater options
farmio Jul 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/components/knx-configure-entity-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { html, nothing } from "lit";

import "@ha/components/ha-alert";
import "@ha/components/ha-card";
import "@ha/components/ha-expansion-panel";
import "@ha/components/ha-selector/ha-selector-select";
import "@ha/components/ha-selector/ha-selector-text";
import "@ha/components/ha-settings-row";
import type { HomeAssistant } from "@ha/types";

import "./knx-sync-state-selector-row";
import "./knx-device-picker";

import { deviceFromIdentifier } from "../utils/device";
import type { BaseEntityData, ErrorDescription } from "../types/entity_data";

export const renderConfigureEntityCard = (
hass: HomeAssistant,
config: Partial<BaseEntityData>,
updateConfig: (ev: CustomEvent) => void,
errors?: ErrorDescription[],
) => {
const device = config.device_info ? deviceFromIdentifier(hass, config.device_info) : undefined;
const deviceName = device ? device.name_by_user ?? device.name : "";
// currently only baseError is possible, others shouldn't be possible due to selectors / optional
const entityBaseError = errors?.find((err) => (err.path ? err.path.length === 0 : true));

return html`
<ha-card outlined>
<h1 class="card-header">Entity configuration</h1>
<p class="card-content">Home Assistant specific settings.</p>
${errors
? entityBaseError
? html`<ha-alert
.alertType=${"error"}
.title=${entityBaseError.error_message}
></ha-alert>`
: nothing
: nothing}
<ha-settings-row narrow>
<div slot="heading">Device</div>
<div slot="description">A device allows to group multiple entities.</div>
<knx-device-picker
.hass=${hass}
.key=${"device_info"}
.value=${config.device_info ?? undefined}
@value-changed=${updateConfig}
></knx-device-picker>
</ha-settings-row>
<ha-settings-row narrow>
<div slot="heading">Name</div>
<div slot="description">Name of the entity.</div>
<ha-selector-text
.hass=${hass}
.label=${"Name"}
.required=${!device}
.selector=${{
text: { type: "text", prefix: deviceName },
}}
.key=${"name"}
.value=${config.name}
@value-changed=${updateConfig}
></ha-selector-text>
</ha-settings-row>
<ha-expansion-panel .header=${"Advanced"} outlined>
<ha-settings-row narrow>
<div slot="heading">Entity settings</div>
<div slot="description">Description</div>
<ha-selector-select
.hass=${hass}
.label=${"Entity category"}
.helper=${"Leave empty for standard behaviour."}
.required=${false}
.selector=${{
select: {
multiple: false,
custom_value: false,
mode: "dropdown",
options: [
{ value: "config", label: "Config" },
{ value: "diagnostic", label: "Diagnostic" },
],
},
}}
.key=${"entity_category"}
.value=${config.entity_category}
@value-changed=${updateConfig}
></ha-selector-select>
</ha-settings-row>
</ha-expansion-panel>
</ha-card>
`;
};
304 changes: 304 additions & 0 deletions src/components/knx-configure-entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import { css, html, LitElement, TemplateResult, nothing } from "lit";
import { customElement, property } from "lit/decorators";

import "@ha/components/ha-card";
import "@ha/components/ha-control-select";
import "@ha/components/ha-svg-icon";
import "@ha/components/ha-expansion-panel";
import "@ha/components/ha-selector/ha-selector";
import "@ha/components/ha-settings-row";

import { fireEvent } from "@ha/common/dom/fire_event";
import type { HomeAssistant } from "@ha/types";

import "./knx-group-address-selector";
import "./knx-sync-state-selector-row";
import { renderConfigureEntityCard } from "./knx-configure-entity-options";
import { KNXLogger } from "../tools/knx-logger";
import { extractValidationErrors } from "../utils/validation";
import type { EntityData, ErrorDescription } from "../types/entity_data";
import type { KNX } from "../types/knx";
import type { PlatformInfo } from "../utils/common";
import type { SettingsGroup, SelectorSchema, GroupSelect } from "../utils/schema";

const logger = new KNXLogger("knx-configure-entity");

@customElement("knx-configure-entity")
export class KNXConfigureEntity extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;

@property({ attribute: false }) public knx!: KNX;

@property({ type: Object }) public platform!: PlatformInfo;

@property({ type: Object }) public config?: EntityData;

@property({ type: Array }) public schema!: SettingsGroup[];

@property({ type: Array }) public validationErrors?: ErrorDescription[];

connectedCallback(): void {
super.connectedCallback();
if (!this.config) {
// fill base keys to get better validation error messages
this.config = { entity: {}, knx: {} };
}
}

protected render(): TemplateResult | void {
const errors = extractValidationErrors(this.validationErrors, "data"); // "data" is root key in our python schema
return html`
<div class="header">
<h1><ha-svg-icon .path=${this.platform.iconPath}></ha-svg-icon>${this.platform.name}</h1>
<p>${this.platform.description}</p>
</div>
<slot name="knx-validation-error"></slot>
<ha-card outlined>
<h1 class="card-header">KNX configuration</h1>
${this.generateRootGroups(this.platform.schema, extractValidationErrors(errors, "knx"))}
</ha-card>
${renderConfigureEntityCard(
this.hass,
this.config!.entity ?? {},
this._updateConfig("entity"),
extractValidationErrors(errors, "entity"),
)}
`;
}

generateRootGroups(schema: SettingsGroup[], errors?: ErrorDescription[]) {
const regular_items: SettingsGroup[] = [];
const advanced_items: SettingsGroup[] = [];

schema.forEach((item: SettingsGroup) => {
if (item.advanced) {
advanced_items.push(item);
} else {
regular_items.push(item);
}
});
return html`
${regular_items.map((group: SettingsGroup) => this._generateSettingsGroup(group, errors))}
${advanced_items.length
? html` <ha-expansion-panel .header=${"Advanced"} outlined>
${advanced_items.map((group: SettingsGroup) =>
this._generateSettingsGroup(group, errors),
)}
</ha-expansion-panel>`
: nothing}
`;
}

_generateSettingsGroup(group: SettingsGroup, errors?: ErrorDescription[]) {
return html` <ha-settings-row narrow>
<div slot="heading">${group.heading}</div>
<div slot="description">${group.description}</div>
${this._generateItems(group.selectors, errors)}
</ha-settings-row>`;
}

_generateItems(selectors: SelectorSchema[], errors?: ErrorDescription[]) {
return html`${selectors.map((selector: SelectorSchema) =>
this._generateItem(selector, errors),
)}`;
}

_generateItem(selector: SelectorSchema, errors?: ErrorDescription[]) {
switch (selector.type) {
case "group_address":
return html`
<knx-group-address-selector
.hass=${this.hass}
.knx=${this.knx}
.key=${selector.name}
.label=${selector.label}
.config=${this.config!.knx[selector.name] ?? {}}
.options=${selector.options}
.validationErrors=${extractValidationErrors(errors, selector.name)}
@value-changed=${this._updateConfig("knx")}
></knx-group-address-selector>
`;
case "selector":
// apply default value if available and no value is set
if (selector.default !== undefined && this.config!.knx[selector.name] == null) {
this.config!.knx[selector.name] = selector.default;
}
return html`
<ha-selector
.hass=${this.hass}
.selector=${selector.selector}
.label=${selector.label}
.helper=${selector.helper}
.key=${selector.name}
.value=${this.config!.knx[selector.name]}
@value-changed=${this._updateConfig("knx")}
></ha-selector>
`;
case "sync_state":
return html`
<knx-sync-state-selector-row
.hass=${this.hass}
.key=${selector.name}
.value=${this.config!.knx[selector.name] ?? true}
.noneValid=${false}
@value-changed=${this._updateConfig("knx")}
></knx-sync-state-selector-row>
`;
case "group_select":
return this._generateGroupSelect(selector, errors);
default:
logger.error("Unknown selector type", selector);
return nothing;
}
}

_generateGroupSelect(selector: GroupSelect, errors?: ErrorDescription[]) {
const value: string =
this.config!.knx[selector.name] ??
// set default if nothing is set yet
(this.config!.knx[selector.name] = selector.options[0].value);
const option = selector.options.find((item) => item.value === value);
if (option === undefined) {
logger.error("No option found for value", value);
}
return html` <ha-control-select
.options=${selector.options}
.value=${value}
.key=${selector.name}
@value-changed=${this._updateConfig("knx")}
></ha-control-select>
${option
? html` <p class="group-description">${option.description}</p>
<div class="group-selection">
${option.schema.map((item: SettingsGroup | SelectorSchema) => {
switch (item.type) {
case "settings_group":
return this._generateSettingsGroup(item, errors);
default:
return this._generateItem(item, errors);
}
})}
</div>`
: nothing}`;
}

private _updateConfig(baseKey: string) {
return (ev) => {
ev.stopPropagation();
if (!this.config[baseKey]) {
this.config[baseKey] = {};
}
this.config[baseKey][ev.target.key] = ev.detail.value;
logger.debug(`update ${baseKey} key "${ev.target.key}" with "${ev.detail.value}"`);
fireEvent(this, "knx-entity-configuration-changed", this.config);
this.requestUpdate();
};
}

static get styles() {
return css`
p {
color: var(--secondary-text-color);
}

.header {
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
padding: 0 16px 16px;

& h1 {
display: inline-flex;
align-items: center;
font-size: 26px;
letter-spacing: -0.012em;
line-height: 48px;
font-weight: normal;
margin-bottom: 14px;

& ha-svg-icon {
color: var(--text-primary-color);
padding: 8px;
background-color: var(--blue-color);
border-radius: 50%;
margin-right: 8px;
}
}

& p {
margin-top: -8px;
line-height: 24px;
}
}

::slotted(ha-alert) {
margin-top: 0 !important;
}

ha-card {
margin-bottom: 24px;
padding: 16px;

& .card-header {
display: inline-flex;
align-items: center;
}
}

ha-settings-row {
padding: 0;
}
ha-control-select {
padding: 0;
margin-bottom: 16px;
}

.group-description {
align-items: center;
margin-top: -8px;
padding-left: 8px;
padding-bottom: 8px;
}

.group-selection {
padding-left: 16px;
padding-right: 16px;
& ha-settings-row:first-child {
border-top: 0;
}
}

knx-group-address-selector,
ha-selector,
ha-selector-text,
ha-selector-select,
knx-sync-state-selector-row,
knx-device-picker {
display: block;
margin-bottom: 16px;
}

ha-alert {
display: block;
margin: 20px auto;
max-width: 720px;

& summary {
padding: 10px;
}
}
`;
}
}

declare global {
interface HTMLElementTagNameMap {
"knx-configure-entity": KNXConfigureEntity;
}
}

declare global {
// for fire event
interface HASSDomEvents {
"knx-entity-configuration-changed": EntityData;
}
}
Loading