diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..b6b1830 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,10 @@ +# Changelog + +## [1.1.0] - Devices +- code integration for faster development +- device and multi entity support +- dual window view (config + discovery) +- fixed config bug + +## [1.0.0] - Initial +- Single Entity creation and discovery creation \ No newline at end of file diff --git a/src/app/_models/mqtt-binary.ts b/src/app/_models/mqtt-binary.ts index f378ffe..14a0226 100644 --- a/src/app/_models/mqtt-binary.ts +++ b/src/app/_models/mqtt-binary.ts @@ -1,7 +1,25 @@ -import { DeviceClass, MQTTEntity } from './mqtt_base'; +import { DeviceClass, iMQTTEntityBase, MQTTEntity } from './mqtt_base'; export class MqttBinary extends MQTTEntity { - dev_cla: DeviceClass = new DeviceClass([ + override ent_type: string = 'binary'; + override attrs: Set = new Set([ + 'name', + 'uniq_id', + 'stat_t', + 'pl_on', + 'pl_off', + 'dev_cla', + ]); + + get dev_cla(): DeviceClass { + return this._dev_cla; + } + + set dev_cla(data: number) { + this._dev_cla.value = data; + } + + _dev_cla: DeviceClass = new DeviceClass([ 'battery', 'battery_charging', 'carbon_monoxide', diff --git a/src/app/_models/mqtt-device.spec.ts b/src/app/_models/mqtt-device.spec.ts new file mode 100644 index 0000000..3e4378c --- /dev/null +++ b/src/app/_models/mqtt-device.spec.ts @@ -0,0 +1,7 @@ +import { MqttDevice } from './mqtt-device'; + +describe('MqttDevice', () => { + it('should create an instance', () => { + expect(new MqttDevice()).toBeTruthy(); + }); +}); diff --git a/src/app/_models/mqtt-device.ts b/src/app/_models/mqtt-device.ts new file mode 100644 index 0000000..90066a6 --- /dev/null +++ b/src/app/_models/mqtt-device.ts @@ -0,0 +1,52 @@ +import { EventEmitter } from "@angular/core"; +import { hash } from "./mqtt_base"; + +export class MQTTDevice { + private _name: string = ''; + private _identifier: string = ''; + private _serial_number: string = ''; + // private configuration_url: string = ''; + + topic_updates = new EventEmitter(); + + get name() { + return this._name; + } + + set name(name: string) { + this._name = name; + if (name == '') this._serial_number = ''; + else this._serial_number = hash(name); + this.topic_updates.next('stat_t'); + this.topic_updates.next('cmd_t'); + this.topic_updates.next('bri_cmd_t'); + } + + get serial_number() { + return this._serial_number; + } + + set serial_number(serial_number: string) { + this._serial_number = serial_number; + } + + get indetifier() { + this._identifier = + this._name == '' && this._serial_number == '' + ? '' + : [this._name, this._serial_number].join('_'); + return this._identifier; + } + + toJSON(full: boolean = false): { [key: string]: string } { + if (full) + return { + ...this.toJSON(), + name: this._name, + serial_number: this._serial_number, + }; + return { + ids: this._identifier, + }; + } + } \ No newline at end of file diff --git a/src/app/_models/mqtt-light.ts b/src/app/_models/mqtt-light.ts index 0313407..1911d19 100644 --- a/src/app/_models/mqtt-light.ts +++ b/src/app/_models/mqtt-light.ts @@ -1,9 +1,56 @@ import { MQTTEntity } from './mqtt_base'; export class MqttLight extends MQTTEntity { - cmd_t: string = 'command/topic'; - bri_cmd_t: string = 'brightness/command/topic'; - pl_on: string = '1'; - pl_off: string = '0'; + pl_on: string = ''; + pl_off: string = ''; val_tpl: string = ''; + bri_cmd_t: string = 'brightness/command/topic'; + _cmd_t: string = 'command/topic'; + + override readonly ent_type: string = 'light'; + override attrs: Set = new Set([ + 'name', + 'uniq_id', + 'stat_t', + 'pl_on', + 'pl_off', + 'val_tpl', + 'bri_cmd_t', + 'cmd_t', + ]); + + get cmd_t() { + return this._cmd_t; + } + set cmd_t(data: string) { + this._cmd_t = data; + } + + override set name(data: string) { + super.name = data; + this.topic_updates.next('cmd_t'); + this.topic_updates.next('bri_cmd_t'); + } + + override get name() { + return this._name; + } + + override set uniq_id(data: string) { + super.uniq_id = data; + this.topic_updates.next('cmd_t'); + this.topic_updates.next('bri_cmd_t'); + } + + override get uniq_id() { + return this._uniq_id; + } + + override publish_topics(index: number = 1): string[] { + return [ + `String stat_topic_${index} = "${this.stat_t}";`, + `String cmd_topic_${index} = "${this.cmd_t}";`, + `String bri_cmd_topic_${index} = "${this.bri_cmd_t}";`, + ]; + } } diff --git a/src/app/_models/mqtt-sensor.ts b/src/app/_models/mqtt-sensor.ts index 369ac49..da995ab 100644 --- a/src/app/_models/mqtt-sensor.ts +++ b/src/app/_models/mqtt-sensor.ts @@ -1,5 +1,16 @@ -import { MQTTEntity } from './mqtt_base'; +import { iMQTTEntityBase, MQTTEntity } from './mqtt_base'; -export class MqttSensor extends MQTTEntity { - unit_of_meas: string = 'meassure'; +export class MqttSensor extends MQTTEntity implements iMqttSensor { + unit_of_meas: string = ''; + override ent_type: string = 'sensor'; + override attrs: Set = new Set([ + 'name', + 'uniq_id', + 'stat_t', + 'unit_of_meas', + ]); +} + +export interface iMqttSensor extends iMQTTEntityBase { + unit_of_meas: string; } diff --git a/src/app/_models/mqtt-switch.ts b/src/app/_models/mqtt-switch.ts index 57c681a..fa90578 100644 --- a/src/app/_models/mqtt-switch.ts +++ b/src/app/_models/mqtt-switch.ts @@ -1,8 +1,57 @@ -import { DeviceClass, MQTTEntity } from './mqtt_base'; +import { DeviceClass, iMQTTEntityBase, MQTTEntity } from './mqtt_base'; export class MqttSwitch extends MQTTEntity { - dev_cla: DeviceClass = new DeviceClass(['switch', 'outlet']); + override ent_type: string = 'switch'; + override attrs: Set = new Set([ + 'name', + 'uniq_id', + 'stat_t', + 'dev_cla', + 'cmd_t', + 'pl_on', + 'pl_off', + ]); + get dev_cla(): DeviceClass { + return this._dev_cla; + } + + set dev_cla(data: number) { + this._dev_cla.value = data; + } + + _dev_cla: DeviceClass = new DeviceClass(['switch', 'outlet']); cmd_t: string = 'command/topic'; - pl_on: string = '1'; - pl_off: string = '0'; + pl_on: string = ''; + pl_off: string = ''; + + override set name(name: string) { + super.name = name; + this.topic_updates.next('cmd_t'); + } + + override set uniq_id(uniq_id: string) { + super.uniq_id = uniq_id; + this.topic_updates.next('cmd_t'); + } + + override get name() { + return this._name; + } + override get uniq_id() { + return this._uniq_id; + } + + override publish_topics(index: number = 1): string[] { + return [ + `String stat_topic_${index} = "${this.stat_t}";`, + `String cmd_topic_${index} = "${this.cmd_t}";`, + ]; + } +} + +export interface iMqttSwitch extends iMQTTEntityBase { + cmd_t: string; + pl_on: string; + pl_off: string; + dev_cla: DeviceClass; } diff --git a/src/app/_models/mqtt_base.ts b/src/app/_models/mqtt_base.ts index 46992a5..f86a402 100644 --- a/src/app/_models/mqtt_base.ts +++ b/src/app/_models/mqtt_base.ts @@ -1,23 +1,58 @@ -export class MQTTEntity { - name: string = ''; - stat_t: string = ''; - uniq_id: string = ''; - // dev: MQTTDevice | null = null; - // entity_type: ENTITY_TYPE = 0; +import { EventEmitter, Injectable } from '@angular/core'; - setProperty(name: unknown, value: any): void { - if (!this.hasOwnProperty(String(name))) return; - this[name as keyof this] = value; +@Injectable() +export class MQTTEntity implements iMQTTEntityBase { + protected _name: string = ''; + protected _stat_t: string = 'state/topic'; + protected _uniq_id: string = ''; + private _size: number = 0; + + attrs = new Set(['name', 'stat_t', 'uniq_id']); + topic_updates = new EventEmitter(); + readonly ent_type: string = 'base'; + + get name() { + return this._name; } - getProperty(name: string): any { - if (!this.hasOwnProperty(name)) return; - return this[name as keyof this]; + set name(name: string) { + this._name = name; + if (name == '') return; + this._uniq_id = name + '_' + hash(name); + this.topic_updates.next('stat_t'); } - toJSON(): any { - let jsonObject: { [key: string]: string } = {}; - for (let prop_name of Object.getOwnPropertyNames(this)) { + get stat_t() { + return this._stat_t; + } + + set stat_t(stat: string) { + this._stat_t = stat; + } + + get uniq_id() { + return this._uniq_id; + } + + set uniq_id(uniq_id: string) { + this._uniq_id = uniq_id; + this.topic_updates.next('stat_t'); + } + + get display_name() { + if (this._uniq_id != '') { + return this._uniq_id; + } + return this._name; + } + + publish_topics(index: number = 1): string[] { + return [`String stat_topic_${index} = "${this.stat_t}";`]; + } + + toJSON(): { [key: string]: string | {} } { + let jsonObject: { [key: string]: string | {} } = {}; + for (let prop_name of this.attrs.values()) { let property = this[prop_name as keyof this]; if (property == '') continue; jsonObject[prop_name] = String(property); @@ -25,20 +60,20 @@ export class MQTTEntity { return jsonObject; } - // toJSON(): any { - // return { - // name: this.name, - // stat_t: this.stat_t, - // uniq_id: this.uniq_id, - // }; - // } -} + get size(): number { + this._size = JSON.stringify(this.toJSON()).length; + return this._size; + } -export class MQTTDevice { - name: string = ''; - identifiers: string[] = ['MQTT']; - serial_number: string = ''; - configuration_url: string = ''; + getProperty(name: string): any { + if (!this.attrs.has(name)) return ''; + return this[name as keyof this]; + } + + setProperty(name: string, value: any) { + if (!this.attrs.has(name)) return; + this[name as keyof this] = value; + } } export class DeviceClass { @@ -53,3 +88,60 @@ export class DeviceClass { return this.choices[this.value]; } } + +export interface iMQTTEntityBase { + name: string; + stat_t: string; + uniq_id: string; + display_name: string; + + attrs: Set; + readonly ent_type: string; + topic_updates: EventEmitter; + + publish_topics: (index: number) => string[]; + toJSON: () => { [key: string]: string | {} }; +} + +export function hash(str: string, digits: number = 5): string { + let seed = cyrb128(str); + let rand = sfc32(seed[0], seed[1], seed[2], seed[3]); + let rand_num = Math.floor(rand() * 10 ** digits).toString(); + return rand_num; +} + +function cyrb128(str: string) { + let h1 = 1779033703, + h2 = 3144134277, + h3 = 1013904242, + h4 = 2773480762; + for (let i = 0, k; i < str.length; i++) { + k = str.charCodeAt(i); + h1 = h2 ^ Math.imul(h1 ^ k, 597399067); + h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); + h3 = h4 ^ Math.imul(h3 ^ k, 951274213); + h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); + } + h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); + h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); + h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); + h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); + (h1 ^= h2 ^ h3 ^ h4), (h2 ^= h1), (h3 ^= h1), (h4 ^= h1); + return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0]; +} + +function sfc32(a: number, b: number, c: number, d: number) { + return function () { + a |= 0; + b |= 0; + c |= 0; + d |= 0; + let t = (((a + b) | 0) + d) | 0; + d = (d + 1) | 0; + a = b ^ (b >>> 9); + b = (c + (c << 3)) | 0; + c = (c << 21) | (c >>> 11); + c = (c + t) | 0; + return (t >>> 0) / 4294967296; + }; +} diff --git a/src/app/_services/generator.service.ts b/src/app/_services/generator.service.ts index 6077039..5abdd63 100644 --- a/src/app/_services/generator.service.ts +++ b/src/app/_services/generator.service.ts @@ -1,51 +1,107 @@ import { EventEmitter, Injectable, Input } from '@angular/core'; -import { MQTTEntity } from '../_models/mqtt_base'; -import { MqttLight } from '../_models/mqtt-light'; -import { MqttSwitch } from '../_models/mqtt-switch'; -import { MqttSensor } from '../_models/mqtt-sensor'; import { MqttBinary } from '../_models/mqtt-binary'; +import { MqttLight } from '../_models/mqtt-light'; +import { MqttSensor } from '../_models/mqtt-sensor'; +import { MqttSwitch } from '../_models/mqtt-switch'; +import { MQTTEntity } from '../_models/mqtt_base'; +import { MQTTDevice } from '../_models/mqtt-device'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class GeneratorService { - public selected_entity: MQTTEntity | null = null; - public created_enteties: Array = []; + public _selected_entity: MQTTEntity | null = null; + public created_enteties: Set = new Set(); - @Input() device_name: string = ""; - @Input() device_id: string = ""; - @Input() device_standalone: boolean = false; - @Input() upperTopic: string = ""; - @Input() auto_topic: number = 2; - updateObserver: EventEmitter = new EventEmitter(); + @Input() auto_topic: boolean = true; + @Input() use_device: boolean = false; + device: MQTTDevice = new MQTTDevice(); + _upper_topic: string = ''; - constructor() { } - - get_properties() { - return Object.getOwnPropertyNames(this.selected_entity); + constructor() { + this.device.topic_updates.subscribe((topic) => { + for (let entity of [...this.created_enteties, this.selected_entity]) { + entity?.setProperty(topic, this.updateTopic(entity, topic)); + } + }); } - update(){ - this.updateObserver.emit(); + get upper_topic(): string { + return this._upper_topic; + } + + set upper_topic(value: string) { + this._upper_topic = value; + for (let entity of [...this.created_enteties, this.selected_entity]) { + entity?.setProperty('stat_t', this.updateTopic(entity, 'stat_t')); + entity?.setProperty('cmd_t', this.updateTopic(entity, 'cmd_t')); + entity?.setProperty('bri_cmd_t', this.updateTopic(entity, 'bri_cmd_t')); + } + } + + updateObserver: EventEmitter = new EventEmitter(); + + set selected_entity(entity: MQTTEntity | null) { + this._selected_entity = entity; + entity?.topic_updates.subscribe((topic: string) => { + if (this.auto_topic) + entity.setProperty(topic, this.updateTopic(entity, topic)); + }); + } + + get selected_entity(): MQTTEntity | null { + return this._selected_entity; + } + + get created_entity_num(): number { + return Array.from(this.created_enteties).length; + } + + updateTopic(entity: MQTTEntity, topic: string) { + let topicStr: string = entity.getProperty(topic).split('/').pop(); + let customTopic = + topicStr == 'topic' || topicStr == topic ? topic : topicStr; + return join( + '/', + this.upper_topic, + this.use_device ? this.device.indetifier : entity.ent_type, + entity.display_name, + customTopic + ).toLowerCase(); + } + + createEntity() { + console.log('Creating entity...'); + if (this._selected_entity) { + this.created_enteties.add(this._selected_entity); + this._selected_entity = null; + } + } + + deleteEntity(entity: MQTTEntity) { + this.created_enteties.delete(entity); } has_property(property: string): boolean { - if (this.selected_entity == null) return false; - if (this.selected_entity.hasOwnProperty(property)) return true; - return false + return false; } + entity_types: { [id: string]: [string, typeof MQTTEntity] } = { + '0': ['select type', MQTTEntity], + '1': ['light', MqttLight], + '2': ['switch', MqttSwitch], + '3': ['sensor', MqttSensor], + '4': ['binary_sensor', MqttBinary], + }; } -export function randomString(length: number): string { - return Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1); -} - -export const entity_types: { [id: string]: [string, typeof MQTTEntity] } = { - '0': ["select type", MQTTEntity], - '1': ["light", MqttLight], - '2': ["switch", MqttSwitch], - '3': ["sensor", MqttSensor], - '4': ["binary_sensor", MqttBinary] +function join(seperator = '/', first: string, ...args: string[]): string { + let result = ''; + for (let str of args) { + if (str == '') continue; + result += seperator + str; + } + if (first == '') return result.slice(1); + return first + result; } diff --git a/src/app/_services/output.service.spec.ts b/src/app/_services/output.service.spec.ts new file mode 100644 index 0000000..4e894f1 --- /dev/null +++ b/src/app/_services/output.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { OutputService } from './output.service'; + +describe('OutputService', () => { + let service: OutputService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OutputService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/_services/output.service.ts b/src/app/_services/output.service.ts new file mode 100644 index 0000000..a6d782a --- /dev/null +++ b/src/app/_services/output.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@angular/core'; +import { MQTTEntity } from '../_models/mqtt_base'; +import { GeneratorService } from './generator.service'; + +@Injectable({ + providedIn: 'root', +}) +export class OutputService { + constructor(private generatorService: GeneratorService) {} + basecode: {} = { 'mqtt-init': '{testestsdsdf}' }; + output: boolean = false; + _integrated_output: boolean = false; + _device_entity_cached: { entity: MQTTEntity; listlen: number } = { + entity: new MQTTEntity(), + listlen: 0, + }; + + get device_entity(): MQTTEntity { + if ( + this._device_entity_cached.listlen === + this.generatorService.created_entity_num + ) + return this._device_entity_cached.entity; + let entites = Array.from(this.generatorService.created_enteties).sort( + (a, b) => { + return a.size - b.size; + } + ); + this._device_entity_cached = { + entity: entites[0], + listlen: this.generatorService.created_entity_num, + }; + console.log('sizing'); + console.log(this._device_entity_cached); + return entites[0]; + } + + get seperate_outputs(): boolean { + return this.output && !this._integrated_output; + } + + get integrated_output(): boolean { + return this.output && this._integrated_output; + } + + set integrated_output(value: boolean) { + this._integrated_output = value; + } + + getDiscoveryString(entity: MQTTEntity, escaped = false): string | {} { + let str = entity.toJSON(); + if (this.generatorService.use_device) { + str['dev'] = this.generatorService.device.toJSON( + entity.uniq_id == this.device_entity.uniq_id + ); + } + + if (escaped) { + return JSON.stringify(str).replaceAll('"', '\\"'); + } + return str; + } + + getDiscoveryTopic(entity: MQTTEntity): string { + return join( + '/', + 'homeassistant', + entity.ent_type, + entity.display_name, + 'config' + ).toLowerCase(); + } + + integrateCode(): string { + let mqtt_init = + '#include \n\nEspMQTTClient mqtt(\n\t"wifi_name",\n\t"wifi_pw",\n\t"broker_ip",\n\t"mqtt_acc",\n\t"mqtt_pw",\n\t"device_name",\n\t"mqtt_port"\n);\n'; + let setup = 'void setup(){\nclient.setMaxPacketSize(512);\n}\n'; + let onConnectStart = 'void onConnectionEstablished() {\n\tdelay=10\n'; + let onConnectEnd = '\tmqtt.loop()\n}\n'; + let loop = 'void loop() {\n\tmqtt.loop();\n\tdelay(100);\n}'; + + let discoveryMsg: string[] = []; + let publishTopics: string[] = []; + for (let [index, entity] of Array.from( + this.generatorService.created_enteties + ).entries()) { + discoveryMsg.push('\t// Sending discovery for Entity ' + index); + discoveryMsg.push( + `\tmqtt.publish(${this.getDiscoveryTopic( + entity + )}, ${this.getDiscoveryString(entity, true)}, true);\n` + ); + + publishTopics.push(...entity.publish_topics(index)); + publishTopics.push(); + } + publishTopics.push('\n'); + return ( + mqtt_init + + '\n' + + publishTopics.join('\n') + + setup + + onConnectStart + + discoveryMsg.join('\n') + + onConnectEnd + + loop + ); + } +} + +function join(seperator = '/', first: string, ...args: string[]): string { + let result = ''; + for (let str of args) { + if (str == '') continue; + result += seperator + str; + } + if (first == '') return result.slice(1); + return first + result; +} diff --git a/src/app/app.component.html b/src/app/app.component.html index 7560e08..744741f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -3,7 +3,7 @@
app icon

MQTT Discovery Creator

-

v.1.0

+

v.1.1

My Homepage Code
diff --git a/src/app/app.module.ts b/src/app/app.module.ts index aa212a9..b8c480c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,26 +1,27 @@ import { NgModule } from '@angular/core'; -import { BrowserModule, provideClientHydration } from '@angular/platform-browser'; +import { + BrowserModule, + provideClientHydration, +} from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { GeneratorComponent } from './generator/generator.component'; import { FormsModule } from '@angular/forms'; -import { EntityComponent } from './entity/entity.component'; +import { EntityDataComponent } from './entity-data/entity-data.component'; +import { EntityOutputComponent } from './entity-output/entity-output.component'; +import { OutputComponent } from './output/output.component'; @NgModule({ declarations: [ AppComponent, GeneratorComponent, - EntityComponent + EntityDataComponent, + EntityOutputComponent, + OutputComponent, ], - imports: [ - BrowserModule, - AppRoutingModule, - FormsModule, - ], - providers: [ - provideClientHydration() - ], - bootstrap: [AppComponent] + imports: [BrowserModule, AppRoutingModule, FormsModule], + providers: [provideClientHydration()], + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/entity-data/entity-data.component.css b/src/app/entity-data/entity-data.component.css new file mode 100644 index 0000000..5a7288b --- /dev/null +++ b/src/app/entity-data/entity-data.component.css @@ -0,0 +1,18 @@ +.heading { + display: flex; + position: inherit; + margin-bottom: 0.25rem; +} + +.heading button { + padding: 0.25rem; + min-width: 0; + position: absolute; + right: 0; + top: 0; +} + +.heading h2 { + width: 100%; + text-align: center; +} diff --git a/src/app/entity-data/entity-data.component.html b/src/app/entity-data/entity-data.component.html new file mode 100644 index 0000000..1bea529 --- /dev/null +++ b/src/app/entity-data/entity-data.component.html @@ -0,0 +1,62 @@ + +
+

entity {{ent_index}} - {{basemodel.ent_type}}

+ +
+
+
+

Name

+ +
+
+

Uniqe ID

+
+ +
+
+
+

Command Topic

+ +
+
+

Brightness Command Topic

+ +
+
+
+

Payload on

+ +
+
+

Payload off

+ +
+
+
+

Unit of meassurement

+ +
+
+

Value Template

+ +
+
+

Device Class

+ +
+
+

State Topic

+
+ +
+
+
+ +
+ +
diff --git a/src/app/entity-data/entity-data.component.spec.ts b/src/app/entity-data/entity-data.component.spec.ts new file mode 100644 index 0000000..cfc7ab7 --- /dev/null +++ b/src/app/entity-data/entity-data.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EntityDataComponent } from './entity-data.component'; + +describe('EntityDataComponent', () => { + let component: EntityDataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [EntityDataComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EntityDataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/entity-data/entity-data.component.ts b/src/app/entity-data/entity-data.component.ts new file mode 100644 index 0000000..7bca260 --- /dev/null +++ b/src/app/entity-data/entity-data.component.ts @@ -0,0 +1,157 @@ +import { Component, Input, ViewChild } from '@angular/core'; +import { DeviceClass, MQTTEntity } from '../_models/mqtt_base'; +import { GeneratorService } from '../_services/generator.service'; + +@Component({ + selector: 'app-entity-data', + templateUrl: './entity-data.component.html', + styleUrl: './entity-data.component.css', +}) +export class EntityDataComponent { + @Input() entity_type: number = 1; + @Input() basemodel!: MQTTEntity; + @Input() created: boolean = false; + @Input() ent_index: number = 0; + + @ViewChild('nameinput') nameinput!: any; + constructor(public generatorService: GeneratorService) { + if (generatorService.selected_entity) { + this.basemodel = generatorService.selected_entity; + } + } + ngAfterViewInit() { + if (!this.created) this.nameinput.nativeElement.focus(); + } + + _entity_name: string = ''; + _entity_uniq_id: string = ''; + _entity_stat_t: string = ''; + _entity_cmd_t: string = ''; + _entity_bri_cmd_t: string = ''; + _entity_pl_off: string = ''; + _entity_pl_on: string = ''; + _entity_unit_of_meas: string = ''; + _entity_val_tpl: string = ''; + _entity_dev_cla: DeviceClass = new DeviceClass([]); + + get entity_name() { + let base = this.basemodel.getProperty('name'); + if (base == '') { + this.basemodel.setProperty('name', this._entity_name); + return this._entity_name; + } + return base; + } + + set entity_name(data: string) { + this._entity_name = data; + this.basemodel.setProperty('name', data); + } + + get entity_uniq_id() { + let base = this.basemodel.getProperty('uniq_id'); + if (base == '') { + this.basemodel.setProperty('uniq_id', this._entity_uniq_id); + return this._entity_uniq_id; + } + return base; + } + + set entity_uniq_id(data: string) { + this._entity_uniq_id = data; + this.basemodel.setProperty('uniq_id', data); + } + + get entity_stat_t() { + let base = this.basemodel.getProperty('stat_t'); + if (base == 'state/topic') { + this.basemodel.setProperty('stat_t', this._entity_stat_t); + return this._entity_stat_t; + } + return base; + } + + set entity_stat_t(data: string) { + this._entity_stat_t = data; + this.basemodel.setProperty('stat_t', data); + } + + get entity_cmd_t() { + let base = this.basemodel.getProperty('cmd_t'); + if (base == 'command/topic') { + this.basemodel.setProperty('cmd_t', this._entity_cmd_t); + return this._entity_cmd_t; + } + return base; + } + + set entity_cmd_t(data: string) { + this._entity_cmd_t = data; + this.basemodel.setProperty('cmd_t', data); + } + + get entity_bri_cmd_t() { + let base = this.basemodel.getProperty('bri_cmd_t'); + if (base == 'brightness/command/topic') { + this.basemodel.setProperty('bri_cmd_t', this._entity_bri_cmd_t); + return this._entity_bri_cmd_t; + } + return base; + } + + set entity_bri_cmd_t(data: string) { + this._entity_bri_cmd_t = data; + this.basemodel.setProperty('bri_cmd_t', data); + } + + get entity_pl_off() { + return this._entity_pl_off; + } + + set entity_pl_off(data: string) { + this._entity_pl_off = data; + this.basemodel.setProperty('pl_off', data); + } + + get entity_pl_on() { + return this._entity_pl_on; + } + + set entity_pl_on(data: string) { + this._entity_pl_on = data; + this.basemodel.setProperty('pl_on', data); + } + + get entity_unit_of_meas() { + return this._entity_unit_of_meas; + } + + set entity_unit_of_meas(data: string) { + this._entity_unit_of_meas = data; + this.basemodel.setProperty('unit_of_meas', data); + } + + get entity_val_tpl() { + return this._entity_val_tpl; + } + + set entity_val_tpl(data: string) { + this._entity_val_tpl = data; + this.basemodel.setProperty('val_tpl', data); + } + + get entity_dev_cla(): number { + this._entity_dev_cla = this.basemodel.getProperty('dev_cla'); + return this._entity_dev_cla.value; + } + + set entity_dev_cla(data: number) { + data = Number(data); + this._entity_dev_cla.value = data; + this.basemodel.setProperty('dev_cla', data); + } + + hasProperty(property: string) { + return this.basemodel.attrs.has(property); + } +} diff --git a/src/app/entity-output/entity-output.component.css b/src/app/entity-output/entity-output.component.css new file mode 100644 index 0000000..bff0f60 --- /dev/null +++ b/src/app/entity-output/entity-output.component.css @@ -0,0 +1,5 @@ +.jsonPrev { + max-width: 50%; + text-wrap: wrap; + flex-shrink: 0; +} diff --git a/src/app/entity-output/entity-output.component.html b/src/app/entity-output/entity-output.component.html new file mode 100644 index 0000000..d372171 --- /dev/null +++ b/src/app/entity-output/entity-output.component.html @@ -0,0 +1,12 @@ + +
+

Discovery String

+
+
{{outputService.getDiscoveryString(basemodel) | json}}
+ {{outputService.getDiscoveryString(basemodel, true)}} +
+
+
+

Discovery Topic

+ +
\ No newline at end of file diff --git a/src/app/entity-output/entity-output.component.spec.ts b/src/app/entity-output/entity-output.component.spec.ts new file mode 100644 index 0000000..fea0300 --- /dev/null +++ b/src/app/entity-output/entity-output.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EntityOutputComponent } from './entity-output.component'; + +describe('EntityOutputComponent', () => { + let component: EntityOutputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [EntityOutputComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EntityOutputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/entity-output/entity-output.component.ts b/src/app/entity-output/entity-output.component.ts new file mode 100644 index 0000000..d6e962c --- /dev/null +++ b/src/app/entity-output/entity-output.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; +import { MQTTEntity } from '../_models/mqtt_base'; +import { OutputService } from '../_services/output.service'; + +@Component({ + selector: 'app-entity-output', + templateUrl: './entity-output.component.html', + styleUrl: './entity-output.component.css', +}) +export class EntityOutputComponent { + constructor(public outputService: OutputService) {} + @Input() basemodel!: MQTTEntity; +} diff --git a/src/app/entity/entity.component.css b/src/app/entity/entity.component.css deleted file mode 100644 index 5a303fc..0000000 --- a/src/app/entity/entity.component.css +++ /dev/null @@ -1,3 +0,0 @@ -pre{ - -} \ No newline at end of file diff --git a/src/app/entity/entity.component.html b/src/app/entity/entity.component.html deleted file mode 100644 index cf2f9e2..0000000 --- a/src/app/entity/entity.component.html +++ /dev/null @@ -1,80 +0,0 @@ -
- -
-

EntityTyp:

- -
- -
-

Name

- -
-
-

Uniqe ID

-
- - -
-
-
-

Command Topic

- -
-
-

Brightness Command Topic

- -
-
-
-

Payload off

- -
-
-

Payload on

- -
-
-
-

Unit of meassurement

- -
-
-

Value Template

- -
-
-

Device Class

- -
- - -
-

State Topic

-
- -
-
- - -
-

Discovery String

-
-
{{discoveryString | json}}
- {{JSONdiscoveryString}} -
-
-
-

Discovery Topic

- -
-
-
\ No newline at end of file diff --git a/src/app/entity/entity.component.ts b/src/app/entity/entity.component.ts deleted file mode 100644 index cd6ecec..0000000 --- a/src/app/entity/entity.component.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { - GeneratorService, - entity_types, - randomString, -} from '../_services/generator.service'; -import { MqttBinary } from '../_models/mqtt-binary'; -import { DeviceClass, MQTTEntity } from '../_models/mqtt_base'; - -@Component({ - selector: 'app-entity', - templateUrl: './entity.component.html', - styleUrl: './entity.component.css', -}) -export class EntityComponent { - constructor(public generatorService: GeneratorService) { - generatorService.updateObserver.subscribe((date) => { - this.update(); - }); - } - - readonly useObject = Object; - readonly entities = entity_types; - readonly useRandomString = randomString; - - showDiscovery: boolean = false; - _discoveryTopic: string = ''; - - @Input() entity_type: number = 0; - @Input() stat_t: string = ''; - @Input() basemodel: MQTTEntity | null = null; - - @Input() entity_name: string = ''; - @Input() entity_uniq_id: string = ''; - @Input() entity_cmd_t: string = ''; - @Input() entity_bri_cmd_t: string = ''; - @Input() entity_pl_off: string = '0'; - @Input() entity_pl_on: string = '1'; - @Input() entity_unit_of_meas: string = ''; - @Input() entity_val_tpl: string = ''; - @Input() entity_dev_cla: DeviceClass = new DeviceClass([]); - - get display_name() { - if (this.entity_name != '' && this.entity_uniq_id != '') { - return this.entity_name + '_' + this.entity_uniq_id; - } else { - return this.entity_name; - } - } - - update(model: keyof this = 'stat_t', event: Event | boolean = false): void { - if (this.generatorService.auto_topic == 2) { - this.stat_t = this.updateTopic('stat'); - this.entity_cmd_t = this.updateTopic('cmd'); - this.entity_bri_cmd_t = this.updateTopic('bri_cmd'); - return; - } - if (event instanceof Event) - var value = (event.target as HTMLInputElement).value; - else return; - if (this.generatorService.auto_topic == 1) { - this.sync_topcis(model, value); - } else { - if (!this.hasOwnProperty(model)) return; - if (typeof this[model] === 'string') { - this[model] = value as (typeof this)[keyof this]; - } - } - } - - sync_topcis(model: keyof this, value: string) { - let new_list = value.split('/'); - let old_list = String(this[model]).split('/'); - let missing = new_list.filter((item) => old_list.indexOf(item) < 0); - - if (old_list.filter((item) => new_list.indexOf(item) < 0).length > 1) - return (this.generatorService.auto_topic = 0); - if (missing.length == 0) return; //(this.generatorService.auto_topic = 2); - if (new_list.indexOf(missing[0]) == new_list.length - 1) { - this[model] = value as (typeof this)[keyof this]; - return; - } - let missing_index = new_list.indexOf(missing[0]); - this.stat_t = this.replaceComponent(this.stat_t, missing[0], missing_index); - this.entity_cmd_t = this.replaceComponent( - this.entity_cmd_t, - missing[0], - missing_index - ); - this.entity_bri_cmd_t = this.replaceComponent( - this.entity_bri_cmd_t, - missing[0], - missing_index - ); - - return; - } - - replaceComponent(object: string, value: string, index: number) { - let param_list = object.split('/'); - param_list[index] = value; - return param_list.join('/'); - } - - updateTopic(topic: string) { - let topic_str = join( - '/', - this.generatorService.upperTopic, - entity_types[this.entity_type][0], - this.generatorService.device_name, - this.display_name, - topic - ).toLowerCase(); - return topic_str; - } - - select_type(event: unknown) { - let ent_type = entity_types[event as keyof typeof entity_types]; - let ent_class = ent_type[1]; - if (typeof ent_class === 'function' && event != 0) { - this.basemodel = new ent_class(); - this.generatorService.selected_entity = this.basemodel; - if (this.generatorService.has_property('dev_cla')) { - this.entity_dev_cla = this.basemodel.getProperty('dev_cla'); - } - } else this.generatorService.selected_entity = null; - this.entity_type = event as number; - this.update(); - } - - lock_auto_topic() { - this.generatorService.auto_topic = 1; - } - - create_entity() { - this.basemodel?.setProperty('stat_t', this.stat_t); - this.basemodel?.setProperty('name', this.entity_name); - this.basemodel?.setProperty('cmd_t', this.entity_cmd_t); - this.basemodel?.setProperty('bri_cmd_t', this.entity_bri_cmd_t); - this.basemodel?.setProperty('pl_off', this.entity_pl_off); - this.basemodel?.setProperty('pl_on', this.entity_pl_on); - this.basemodel?.setProperty('unit_of_meas', this.entity_unit_of_meas); - this.basemodel?.setProperty('val_tpl', this.entity_val_tpl); - this.basemodel?.setProperty('dev_cla', this.entity_dev_cla); - this._discoveryTopic = this.discoveryTopic; - this.showDiscovery = true; - return; - } - - get discoveryString() { - return this.basemodel?.toJSON(); - } - - get JSONdiscoveryString() { - return JSON.stringify(this.basemodel?.toJSON()).replaceAll('"', '\\"'); - } - - get discoveryTopic() { - if (this.entity_type == 0) return ''; - return join( - '/', - 'homeassistant', - entity_types[this.entity_type][0], - this.display_name - ).toLowerCase(); - } -} - -function join(seperator = '/', first: string, ...args: string[]): string { - let result = ''; - for (let str of args) { - if (str == '') continue; - result += seperator + str; - } - if (first == '') return result.slice(1); - return first + result; -} diff --git a/src/app/generator/generator.component.css b/src/app/generator/generator.component.css index cd945ae..2336ade 100644 --- a/src/app/generator/generator.component.css +++ b/src/app/generator/generator.component.css @@ -1,32 +1,53 @@ +#mainContainer { + display: grid; + grid-template-columns: 3fr 1fr; + gap: 0.5rem; + transition: all 0.25s; +} + +.genContainer, +.outContainer { + background: #9d9d9d; + border-radius: 1rem; + padding: 0.75rem; + gap: 1rem; +} + .genContainer { - background: #9D9D9D; - border-radius: 1rem; - padding: 1rem .8rem ; - display: flex; - flex-direction: column; - gap: 1rem; + grid-column: 1; + position: relative; } -.customCheckboxContainer{ - display: flex; - flex-direction: row; - gap: .5rem; +.outContainer { + display: flex; + flex-direction: column; + grid-column: 2; } -.customCheckboxContainer input[type="radio"]{ - display: none; +.customCheckboxContainer { + display: flex; + flex-direction: row; + gap: 0.5rem; } -.customCheckbox{ - width: 100%; - padding: .25rem 0; - text-align: center; - background: #B3B3B3; - transition: background .25s ease-in-out, color .25s ease-in-out; - user-select: none; +.customCheckboxContainer input { + display: none; } -.customCheckboxContainer input:checked + label{ - background: var(--accent); - color: var(--secondary); -} \ No newline at end of file +.customCheckbox { + width: 100%; + padding: 0.25rem 0; + text-align: center; + background: #b3b3b3; + transition: background 0.25s ease-in-out, color 0.25s ease-in-out; + user-select: none; +} + +.customCheckboxContainer input:checked + label { + background: var(--accent); + color: var(--secondary); +} + +.outEnable { + grid-template-columns: 1fr 1fr !important; +} diff --git a/src/app/generator/generator.component.html b/src/app/generator/generator.component.html index 2032dbb..46ae0ab 100644 --- a/src/app/generator/generator.component.html +++ b/src/app/generator/generator.component.html @@ -1,28 +1,74 @@ -
-
+
+
-
+

Bereich

- +
-
-

Automatische Topics

-
- - - - - - +
+

Device

+
+ +
+
+

Automatic Topics

+
+ + +
+
+ +
+

Device Name

+ +
+
+

Device Identifier

+ +
+ +
+
+

EntityTyp:

+ +
- +
+

Output

+
+ + +
+

Seperate Output

+
+ + + +
+
+ + + + + + + + +
\ No newline at end of file diff --git a/src/app/generator/generator.component.ts b/src/app/generator/generator.component.ts index 7e9f1f4..31d613e 100644 --- a/src/app/generator/generator.component.ts +++ b/src/app/generator/generator.component.ts @@ -1,44 +1,49 @@ -import { Component, Input } from '@angular/core'; -import { MqttLight } from '../_models/mqtt-light'; -import { MqttSwitch } from '../_models/mqtt-switch'; -import { MqttSensor } from '../_models/mqtt-sensor'; -import { MqttBinary } from '../_models/mqtt-binary'; -import { GeneratorService, entity_types, randomString } from '../_services/generator.service'; +import { Component, Input, ViewChild } from '@angular/core'; +import { GeneratorService } from '../_services/generator.service'; import { MQTTEntity } from '../_models/mqtt_base'; +import { OutputService } from '../_services/output.service'; +import { MQTTDevice } from '../_models/mqtt-device'; @Component({ selector: 'app-home', templateUrl: './generator.component.html', - styleUrl: './generator.component.css' + styleUrl: './generator.component.css', }) - export class GeneratorComponent { - constructor(public generatorService: GeneratorService) { - } - + constructor( + public generatorService: GeneratorService, + public outputService: OutputService + ) {} readonly useObject = Object; - readonly useRandomString = randomString; - set auto_topic(value: number) { - this.generatorService.auto_topic = value; - console.log(this.generatorService.auto_topic); + @ViewChild('typeinput') typeinput: any; + @Input() entity_type: number = 0; + + device: MQTTDevice = this.generatorService.device; + + get created_entities(): MQTTEntity[] { + return Array.from(this.generatorService.created_enteties); } - get auto_topic(): string { - return String(this.generatorService.auto_topic); + get codeSpan(): string { + let start = this.generatorService.selected_entity == null ? 3 : 2; + return `${start}/${3 + this.generatorService.created_entity_num}`; } - log(event:any){ - console.log(Number(event)); + select_type(entity_num: number) { + this.entity_type = entity_num; + let entity_type = + this.generatorService.entity_types[ + entity_num as keyof typeof this.generatorService.entity_types + ]; + let entity_class = entity_type[1]; + + if (typeof entity_class === 'function' && entity_num != 0) { + this.generatorService.selected_entity = new entity_class(); + } else { + this.generatorService.selected_entity = null; + this.typeinput.nativeElement.focus(); + } } - - } - - - - - - - diff --git a/src/app/output/output.component.css b/src/app/output/output.component.css new file mode 100644 index 0000000..a10880b --- /dev/null +++ b/src/app/output/output.component.css @@ -0,0 +1,5 @@ +pre { + max-width: calc(50vw - 3rem); + overflow-x: scroll; + text-wrap: nowrap; +} diff --git a/src/app/output/output.component.html b/src/app/output/output.component.html new file mode 100644 index 0000000..2cafb6a --- /dev/null +++ b/src/app/output/output.component.html @@ -0,0 +1,5 @@ +
+

Code Output

+
{{outputService.integrateCode()}}
+

+
diff --git a/src/app/entity/entity.component.spec.ts b/src/app/output/output.component.spec.ts similarity index 55% rename from src/app/entity/entity.component.spec.ts rename to src/app/output/output.component.spec.ts index d9e8dfb..6ac7b51 100644 --- a/src/app/entity/entity.component.spec.ts +++ b/src/app/output/output.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { EntityComponent } from './entity.component'; +import { OutputComponent } from './output.component'; -describe('EntityComponent', () => { - let component: EntityComponent; - let fixture: ComponentFixture; +describe('OutputComponent', () => { + let component: OutputComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [EntityComponent] + declarations: [OutputComponent] }) .compileComponents(); - fixture = TestBed.createComponent(EntityComponent); + fixture = TestBed.createComponent(OutputComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/output/output.component.ts b/src/app/output/output.component.ts new file mode 100644 index 0000000..dc5a3f8 --- /dev/null +++ b/src/app/output/output.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { OutputService } from '../_services/output.service'; + +@Component({ + selector: 'app-output', + templateUrl: './output.component.html', + styleUrl: './output.component.css', +}) +export class OutputComponent { + constructor(public outputService: OutputService) {} +} diff --git a/src/styles.css b/src/styles.css index 1900866..5b778e2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -3,81 +3,105 @@ @tailwind components; @tailwind utilities; -:root{ - --accent: #4cb926; - --text: #535353; - --primary: #b3b3b3; - --secondary: #f8f8f8; - +:root { + --accent: #4cb926; + --text: #3d3d3d; + --primary: #b3b3b3; + --secondary: #f8f8f8; + --placeholder: #777; + --input_bg: #b3b3b3; } -input, select, label, pre { - background: var(--primary); - padding: .25rem .75rem; - border-radius: 1rem; +input, +select, +label, +pre, +button { + background: var(--primary); + padding: 0.25rem 0.75rem; + border-radius: 1rem; } -input, select{ - width: 100%; - @apply focus:outline focus:outline-myAccent focus:outline-2; +pre { + text-wrap: wrap; } -input::placeholder{ - color: var(--text); - opacity: 1; +input, +select { + width: 100%; + @apply focus:outline focus:outline-myAccent focus:outline-2; } -input::-ms-input-placeholder { /* Edge 12 -18 */ - color: var(--text); - } - -svg{ - height: 90%; - margin: auto 0; - width: auto; +span { + @apply focus-visible:outline focus-visible:outline-myAccent focus-visible:outline-2; } -*{ - color: var(--text); +input::placeholder { + color: var(--placeholder); + opacity: 1; } -button{ - background-color: var(--accent); - border-radius: 1rem; - color: var(--secondary); - font-weight: bold; - min-width: 4rem; +input::-ms-input-placeholder { + /* Edge 12 -18 */ + color: var(--placeholder); } -.randomButton{ - @apply flex justify-center content-center p-1; +svg { + height: 90%; + margin: auto 0; + width: auto; +} + +* { + color: var(--text); +} + +button { + background-color: var(--accent); + border-radius: 1rem; + color: var(--secondary); + font-weight: bold; + min-width: 4rem; } button path { - color: var(--secondary); + color: var(--secondary); } .property { - background-color: #9D9D9D; - border-radius: .5rem; - /* padding: .5rem; */ - display: inline-flex; - flex-direction: column; - gap: .25rem; + /* background-color: #9d9d9d; */ + border-radius: 0.5rem; + /* padding: .5rem; */ + display: inline-flex; + flex-direction: column; + gap: 0.25rem; } -.property p, .property h3{ - align-self: left; - padding-left: .5rem; +.property p, +.property h3 { + align-self: left; + padding-left: 0.5rem; } -.property > div { - width: 100%; +div.property { + width: 100%; } .property span { - background: #B3B3B3; - padding: .25rem .75rem; - border-radius: 1rem; - /* width: 100%; */ -} \ No newline at end of file + background: var(--input_bg); + padding: 0.25rem 0.75rem; + border-radius: 1rem; + /* width: 100%; */ +} + +#entityPlaceholder { + background: transparent; + border: 2px dotted var(--input_bg); + color: var(--input_bg); + font-weight: normal; +} + +#entityPlaceholder:hover { + border-color: var(--secondary); + color: var(--secondary); +}