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/README.md b/README.md index 8ecdd5e..f90300e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,34 @@ # MqttCreator -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.4. +This project is a simple and incomplete generator to create a Homeassinstant compatible MQTT Autodiscovery Entity + +## Limited Coverage +By now only the these entities (binary_sensor, light, sensor, switch) are supported by the generator, because they are the most commmonly used entities for my own needs + +## Usage +The UI is seperated into two parts, the global generator with settings and upper Topic and the entity itselt + +### Generator: +Automated Topic Settings: +- all: the topics will be generated automatically from the other supplied inputs, all changes will be wiped +- only changes: user inputs will be synced accross all topics (only works if the general structure stays the same and changes are not to large at once) +- none: everything is done manually + +### Entity: +After choosing an entity type, change and input the corresponding values as you like and if automated topics is actived the topics will be populated automatically + +Once done, you can press the DiscoveryString button. + +### Discovery String: +First you see the discovery string formatted, there you can check for any errors in the configuration + +Below there will be a discovery string with escaped perentecies to input into print statements or similar + +The last field Discovery Topic is the automatically generated Discovery Topic for default Homeassistant MQTT Autodiscovery + + + + diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..86edac4 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,7 @@ +services: + mqtt_creator: + image: git.letsstein.de/tom/mqtt_creator:latest + restart: unless-stopped + ports: + - "${PORT}:80" + \ No newline at end of file diff --git a/src/app/_models/mqtt-binary.ts b/src/app/_models/mqtt-binary.ts index 35becac..14a0226 100644 --- a/src/app/_models/mqtt-binary.ts +++ b/src/app/_models/mqtt-binary.ts @@ -1,7 +1,54 @@ -import { DEVICE_CLASS, MQTTEntity } from "./mqtt_base"; +import { DeviceClass, iMQTTEntityBase, MQTTEntity } from './mqtt_base'; export class MqttBinary extends MQTTEntity { - dev_cla: DEVICE_CLASS = 0 - pl_on: string = "1"; - pl_off: string = "0"; + 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', + 'cold', + 'connectivity', + 'door', + 'garage_door', + 'gas', + 'heat', + 'light', + 'lock', + 'moisture', + 'motion', + 'moving', + 'occupancy', + 'opening', + 'plug', + 'power', + 'presence', + 'problem', + 'running', + 'safety', + 'smoke', + 'sound', + 'tamper', + 'update', + 'vibration', + 'window', + ]); + pl_on: string = '1'; + pl_off: string = '0'; } 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 b45dd55..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"; +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"; - val_tpl: string = ""; -} \ No newline at end of file + 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 d95aa19..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"; - } \ No newline at end of file +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 a3d9503..fa90578 100644 --- a/src/app/_models/mqtt-switch.ts +++ b/src/app/_models/mqtt-switch.ts @@ -1,8 +1,57 @@ -import { DEVICE_CLASS, MQTTEntity } from "./mqtt_base"; +import { DeviceClass, iMQTTEntityBase, MQTTEntity } from './mqtt_base'; export class MqttSwitch extends MQTTEntity { - dev_cla: DEVICE_CLASS = 0 - cmd_t: string = "command/topic"; - pl_on: string = "1"; - pl_off: string = "0"; -} \ No newline at end of file + 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 = ''; + 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 72c8024..f86a402 100644 --- a/src/app/_models/mqtt_base.ts +++ b/src/app/_models/mqtt_base.ts @@ -1,43 +1,147 @@ -export class MQTTEntity { - name: string = ""; - stat_t: string = "state/topic"; - uniq_id: string = "unique_id"; - 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; - createString() : string{ - let string: string = ""; - for(let property of Object.getOwnPropertyNames(this)){ - if(this[property as keyof this] == null) continue; - string += `"${property}": "${this[property as keyof this]}",` - } - console.log(string); - return string; + attrs = new Set(['name', 'stat_t', 'uniq_id']); + topic_updates = new EventEmitter(); + readonly ent_type: string = 'base'; + + get name() { + return this._name; + } + + set name(name: string) { + this._name = name; + if (name == '') return; + this._uniq_id = name + '_' + hash(name); + this.topic_updates.next('stat_t'); + } + + 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); + } + return jsonObject; + } + + get size(): number { + this._size = JSON.stringify(this.toJSON()).length; + return this._size; + } + + 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 MQTTDevice { - name: string = ""; - identifiers: string[] = ["MQTT"]; - serial_number: string = ""; - configuration_url: string = ""; +export class DeviceClass { + value: number = 0; + choices: string[] = ['None']; + + constructor(choices: string[]) { + this.choices = choices; + } + + toString(): string { + return this.choices[this.value]; + } } -// export enum ENTITY_TYPE { -// light = 0, -// switch = 1, -// sensor = 2, -// binary_sensor = 3, -// button = 4, -// } +export interface iMQTTEntityBase { + name: string; + stat_t: string; + uniq_id: string; + display_name: string; -export enum DEVICE_CLASS { - motion = 0, - movement = 1, - outlet = 2 - } \ No newline at end of file + 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 8293510..5abdd63 100644 --- a/src/app/_services/generator.service.ts +++ b/src/app/_services/generator.service.ts @@ -1,50 +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 = ""; - 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 d63a16c..744741f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,3 +1,11 @@ -
- -
\ No newline at end of file + +
+
+ app icon +

MQTT Discovery Creator

+

v.1.1

+ My Homepage + Code +
+ +
\ No newline at end of file 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 e95a034..0000000 --- a/src/app/entity/entity.component.css +++ /dev/null @@ -1,27 +0,0 @@ -.property { - background-color: #9D9D9D; - border-radius: .5rem; - padding: .5rem; - display: inline-flex; - flex-direction: column; - column-gap: 1rem; -} - -.property p{ - align-self: center; -} - -.property > div { - width: 100%; -} - -.property span { - background: #B3B3B3; - padding: .25rem .75rem; - border-radius: 1rem; - /* width: 100%; */ -} - -.buttonOff path{ - fill:#535353; -} \ 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 7fda1d1..0000000 --- a/src/app/entity/entity.component.html +++ /dev/null @@ -1,81 +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}} -
-
-

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 245446c..0000000 --- a/src/app/entity/entity.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { GeneratorService, entity_types, randomString } from '../_services/generator.service'; -import { MqttBinary } from '../_models/mqtt-binary'; -import { 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.updateStateTopic(); - }) - } - - readonly useObject = Object; - readonly entities = entity_types; - readonly useRandomString = randomString; - - auto_stat_t: boolean = true; - showDiscovery: boolean = false; - - @Input() entity_type: number = 0; - @Input() state_topic: 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 = ""; - @Input() entity_pl_on: string = ""; - @Input() entity_unit_of_meas: string = ""; - @Input() entity_val_tpl: string = ""; - @Input() entity_dev_cla: string = ""; - - updateStateTopic() { - if (!this.auto_stat_t) return - this.state_topic = "" - if (this.generatorService.upperTopic != "") this.state_topic += this.generatorService.upperTopic + "/"; - if (this.entity_type != 0) this.state_topic += entity_types[this.entity_type][0] + "/" - if (this.generatorService.device_name != "") this.state_topic += this.generatorService.device_name + "/" - if (this.entity_name != "" && this.entity_uniq_id != "") this.state_topic += this.entity_name + "_" + this.entity_uniq_id + "/stat" - else if (this.entity_name != "") this.state_topic += this.entity_name + "/stat" - this.state_topic = this.state_topic.toLocaleLowerCase(); - } - - basemodelProperty(property: string) { - if (this.basemodel?.hasOwnProperty(property)) console.log(property) - return this.basemodel?.stat_t - } - - 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 - } else this.generatorService.selected_entity = null - this.entity_type = event as number; - this.updateStateTopic(); - } - - lockStateTopic(event: any) { - this.auto_stat_t = false; - } - - create_entity() { - this.basemodel?.setProperty('stat_t', this.state_topic); - - 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.showDiscovery = true; - return - } - - get discoveryString() { - let discString = this.basemodel?.createString() - if (discString == "" || discString == undefined) return ""; - discString = discString.replaceAll('"', '\\"') - return "{" + discString + "}" - } - - get discoveryTopic() { - if (this.entity_type == 0) return ""; - if (this.entity_name == "") return ""; - let discTopic = "homeassistant/" + entity_types[this.entity_type][0] + "/" + this.entity_name + "_" + this.entity_uniq_id + "/config" - return discTopic; - } -} diff --git a/src/app/generator/generator.component.css b/src/app/generator/generator.component.css index f6f266f..2336ade 100644 --- a/src/app/generator/generator.component.css +++ b/src/app/generator/generator.component.css @@ -1,5 +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 ; -} \ No newline at end of file + grid-column: 1; + position: relative; +} + +.outContainer { + display: flex; + flex-direction: column; + grid-column: 2; +} + +.customCheckboxContainer { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.customCheckboxContainer input { + display: none; +} + +.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 75bad3f..46ae0ab 100644 --- a/src/app/generator/generator.component.html +++ b/src/app/generator/generator.component.html @@ -1,31 +1,74 @@ -
-
- app icon -

MQTT Discovery Creator

-
- - -
+ +
+
-

Data Channel

-
-

Bereich

- + + +
+

Bereich

+ +
+
+

Device

+
+ + +
+
+
+

Automatic Topics

+
+ + +
+
+ +
+

Device Name

+ +
+
+

Device Identifier

+ +
+ +
+
+

EntityTyp:

+
-
- + -
\ No newline at end of file + +
+

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 0eb97a1..31d613e 100644 --- a/src/app/generator/generator.component.ts +++ b/src/app/generator/generator.component.ts @@ -1,33 +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; + @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 codeSpan(): string { + let start = this.generatorService.selected_entity == null ? 3 : 2; + return `${start}/${3 + this.generatorService.created_entity_num}`; + } + + 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 a2e191d..5b778e2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -3,45 +3,105 @@ @tailwind components; @tailwind utilities; -input, select { - background: #B3B3B3; - padding: .25rem .75rem; - border-radius: 1rem; - width: 100%; - @apply focus:outline focus:outline-myAccent focus:outline-2; +:root { + --accent: #4cb926; + --text: #3d3d3d; + --primary: #b3b3b3; + --secondary: #f8f8f8; + --placeholder: #777; + --input_bg: #b3b3b3; } -input::placeholder{ - color: #535353; - opacity: 1; +input, +select, +label, +pre, +button { + background: var(--primary); + padding: 0.25rem 0.75rem; + border-radius: 1rem; } -input::-ms-input-placeholder { /* Edge 12 -18 */ - color: #535353; - } - -svg{ - height: 90%; - margin: auto 0; - width: auto; +pre { + text-wrap: wrap; } -*{ - color: #535353; +input, +select { + width: 100%; + @apply focus:outline focus:outline-myAccent focus:outline-2; } -button, label{ - background-color: #4CB926; - border-radius: 1rem; - color: #F8F8F8; - font-weight: bold; - min-width: 4rem; +span { + @apply focus-visible:outline focus-visible:outline-myAccent focus-visible:outline-2; } -.randomButton{ - @apply flex justify-center content-center p-1; +input::placeholder { + color: var(--placeholder); + opacity: 1; +} + +input::-ms-input-placeholder { + /* Edge 12 -18 */ + color: var(--placeholder); +} + +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: #F8F8F8; -} \ No newline at end of file + color: var(--secondary); +} + +.property { + /* 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: 0.5rem; +} + +div.property { + width: 100%; +} + +.property span { + 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); +}