HomeBuilder - Initial contribution (#172)

openHAB Home Builder - home structure generator

Also-by: Thomas Dietrich <thomas.dietrich@tu-ilmenau.de> (github: ThomDietrich)
Also-by: Demyan Rogozhin <dmitriy.rogozhin@gmail.com> (github: demyanrogozhin)
Signed-off-by: Kuba Wolanin <hi@kubawolanin.com> (github: kubawolanin)
This commit is contained in:
Kuba Wolanin
2017-12-03 00:13:10 +01:00
committed by Kai Kreuzer
parent 714bc1d2a8
commit 3d811ecede
45 changed files with 8594 additions and 0 deletions
+2
View File
@@ -4,3 +4,5 @@
/*features*/*/src/main/history/ /*features*/*/src/main/history/
maven-metadata-local.xml maven-metadata-local.xml
dependency-reduced-pom.xml dependency-reduced-pom.xml
**/node_modules/
npm-debug.log
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>
@@ -0,0 +1,40 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": false
},
"sourceType": "module"
},
"rules": {
"no-cond-assign": 1,
"no-debugger": 1,
"no-dupe-args": 1,
"no-dupe-keys": 1,
"no-duplicate-case": 1,
"no-unreachable": 1,
"use-isnan": 1,
"valid-typeof": 1,
"block-scoped-var": 1,
"curly": [1,"all"],
"default-case": 1,
"eqeqeq": [1,"smart"],
"guard-for-in": 1,
"no-alert": 1,
"no-else-return": 1,
"no-empty-label": 1,
"no-eval": 1,
"no-labels": 1,
"radix": 1,
"no-undef": 1,
"no-unused-vars": 1,
"no-const-assign": 1,
"no-this-before-super": 1,
"constructor-super": 1
}
}
@@ -0,0 +1 @@
* text=auto
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.ui.homebuilder</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ManifestBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.SchemaBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ds.core.builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.PluginNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
@@ -0,0 +1,22 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: HomeBuilder User Interface
Bundle-SymbolicName: org.openhab.ui.homebuilder
Bundle-Version: 2.2.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Service-Component: OSGI-INF/*.xml
Import-Package: com.google.gson,
com.google.gson.annotations,
javax.servlet,
javax.servlet.http,
javax.ws.rs,
javax.ws.rs.core,
org.eclipse.smarthome.io.rest,
org.openhab.ui.dashboard,
org.osgi.framework,
org.osgi.service.cm,
org.osgi.service.http,
org.osgi.service.component.annotations;resolution:=optional,
org.slf4j
Bundle-Vendor: openHAB
Bundle-ActivationPolicy: lazy
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2015-2017 by the respective copyright holders.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/epl-v10.html
-->
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="activate" deactivate="deactivate" immediate="true" name="org.openhab.homebuilder">
<implementation class="org.openhab.ui.homebuilder.internal.HomeBuilderDashboardTile"/>
<reference bind="setHttpService" cardinality="1..1" interface="org.osgi.service.http.HttpService" name="HttpService" policy="static" unbind="unsetHttpService"/>
<service>
<provide interface="org.openhab.ui.dashboard.DashboardTile"/>
</service>
</scr:component>
@@ -0,0 +1,32 @@
# Home Builder
> Boilerplate for the [Items](http://docs.openhab.org/configuration/items.html), [sitemap](http://docs.openhab.org/configuration/sitemaps.html) files and [HABPanel](http://docs.openhab.org/addons/uis/habpanel/readme.html) dashboard.
## Usage
See [USAGE.md](USAGE.md) file for reference.
## Development
HomeBuilder UI is an app based on [Vue.js](http://vuejs.org) framework.
In order to setup the development environment simply run:
``` bash
cd web
npm install
npm run dev
```
The first command will install all necessary dependencies (including Webpack build system).
`npm run dev`, however, will serve the build with hot reload at `http://localhost:8080`.
It's recommended to debug the app on Chrome or Firefox with [Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) extension.
When you're done with your changes and would like to test HomeBuilder on a real environment, run the following command:
``` bash
# build for production with minification
npm run build
```
It will build the app inside `/web/dist/` folder with source map included.
@@ -0,0 +1,18 @@
# Home Builder
> Boilerplate for the [Items](http://docs.openhab.org/configuration/items.html), [sitemap](http://docs.openhab.org/configuration/sitemaps.html) files and [HABPanel](http://docs.openhab.org/addons/uis/habpanel/readme.html) dashboard.
## Items
The tool allows you to generate openHAB Items for your home structure.
You can choose to generate textual `*.items` file content or construct a request directly to the REST API that'll create the items for you.
### Features
- Classifies the objects within each room and creates groups for them
- Optionally adds icons from [Classic Icon Set](http://docs.openhab.org/addons/iconsets/classic/readme.html) to the items
- Optionally adds Tags to the items - convenient for [HomeKit](http://docs.openhab.org/addons/io/homekit/readme.html)/[Hue Emulation](http://docs.openhab.org/addons/io/hueemulation/readme.html#device-tagging) add-ons users
- Automatically aligns the items vertically
- Generates a [Sitemap](http://docs.openhab.org/configuration/sitemaps.html) file
- Generates a set of HABPanel Dashboards corresponding with the Items
@@ -0,0 +1,68 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
<title>About</title>
</head>
<body lang="EN-US">
<h2>About This Content</h2>
<p>&lt;<em>August 10, 2017</em>&gt;</p>
<h3>License</h3>
<p>The openHAB community makes available all content in this plug-in (&quot;Content&quot;). Unless otherwise
indicated below, the Content is provided to you under the terms and conditions of the
Eclipse Public License Version 1.0 (&quot;EPL&quot;). A copy of the EPL is available
at <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>.
For purposes of the EPL, &quot;Program&quot; will mean the Content.</p>
<p>If you did not receive this Content directly from the openHAB community, the Content is
being redistributed by another party (&quot;Redistributor&quot;) and different terms and conditions may
apply to your use of any object code in the Content. Check the Redistributor's license that was
provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
indicated below, the terms and conditions of the EPL still apply to any source code in the Content
and such source code may be obtained at <a href="http://www.openhab.org/">openhab.org</a>.</p>
<h3>Third Party Content</h3>
<p>The Content includes items that have been sourced from third parties as set out below. If you
did not receive this Content directly from the openHAB community, the following is provided
for informational purposes only, and you should look to the Redistributor's license for
terms and conditions of use.</p>
<p><em>
<strong>ascii-table</strong> <br/><br/>
<a href="https://www.npmjs.com/package/ascii-table">ascii-table</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
<p><em>
<strong>bootstrap-vue</strong> <br/><br/>
<a href="https://www.npmjs.com/package/bootstrap-vue">bootstrap-vue</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
<p><em>
<strong>lodash</strong> <br/><br/>
<a href="https://www.npmjs.com/package/lodash">lodash</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
<p><em>
<strong>underscore.string</strong> <br/><br/>
<a href="https://www.npmjs.com/package/underscore.string">underscore.string</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
<p><em>
<strong>vue</strong> <br/><br/>
<a href="https://www.npmjs.com/package/vue">vue</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
<p><em>
<strong>vue-form-generator</strong> <br/><br/>
<a href="https://www.npmjs.com/package/vue-form-generator">vue-form-generator</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
<p><em>
<strong>vue-i18n</strong> <br/><br/>
<a href="https://www.npmjs.com/package/vue-i18n">vue-i18n</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
<p><em>
<strong>vue-multiselect</strong> <br/><br/>
<a href="https://www.npmjs.com/package/vue-multiselect">vue-multiselect</a> - a subject to the <a href="https://spdx.org/licenses/MIT.html">MIT License</a>
</em></p>
</body>
</html>
@@ -0,0 +1,14 @@
bin.includes = META-INF/,\
.,\
about.html,\
OSGI-INF/,\
web/
output = target/classes/
source.. = src/main/java/
bin.excludes = web/node_modules/,\
web/src/,\
web/dist/build.js.map,\
web/webpack.config.js,\
web/.babelrc,\
web/.eslintrc.json,\
web/package.json
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.openhab.core</groupId>
<artifactId>pom-bundles</artifactId>
<version>2.2.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>org.openhab.ui.homebuilder</artifactId>
<name>openHAB HomeBuilder UI Dashboard Integration</name>
<packaging>eclipse-plugin</packaging>
</project>
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2015-2017 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.ui.homebuilder.internal;
import org.openhab.ui.dashboard.DashboardTile;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The dashboard tile and resource registering for Home Builder
*
* @author Kuba Wolanin - Initial contribution
*
*/
public class HomeBuilderDashboardTile implements DashboardTile {
public static final String HOMEBUILDER_ALIAS = "/homebuilder";
private final Logger logger = LoggerFactory.getLogger(HomeBuilderDashboardTile.class);
protected HttpService httpService;
protected void activate() {
try {
httpService.registerResources(HOMEBUILDER_ALIAS, "web", null);
logger.info("Started Home Builder at {}", HOMEBUILDER_ALIAS);
} catch (NamespaceException e) {
logger.error("Error during Home Builder startup: {}", e.getMessage());
}
}
protected void deactivate() {
httpService.unregister(HOMEBUILDER_ALIAS);
logger.info("Stopped Home Builder");
}
protected void setHttpService(HttpService httpService) {
this.httpService = httpService;
}
protected void unsetHttpService(HttpService httpService) {
this.httpService = null;
}
@Override
public String getName() {
return "Home Builder";
}
@Override
public String getUrl() {
return "../homebuilder/index.html";
}
@Override
public String getOverlay() {
return null;
}
@Override
public String getImageUrl() {
return "../homebuilder/tile.png";
}
}
@@ -0,0 +1,5 @@
{
"presets": [
["env", { "modules": false }]
]
}
@@ -0,0 +1,3 @@
node_modules/
npm-debug.log
yarn-error.log
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,65 @@
{
"GroundFloor": "Erdgeschoss",
"FirstFloor": "Erster Stock",
"SecondFloor": "Zweiter Stock",
"ThirdFloor": "Dritter Stock",
"FourthFloor": "Vierter Stock",
"Attic": "Dachgeschoss",
"Balcony": "Balkon",
"Backyard": "Hinterhof",
"Basement": "Keller",
"Bathroom": "Badezimmer",
"Bedroom": "Schlafzimmer",
"Boiler": "Heizungsraum",
"Wardrobe": "Garderobe",
"Cellar": "Keller",
"Corridor": "Gang",
"Deck": "Deck",
"Dining": "Esszimmer",
"Downstairs": "Unten",
"Driveway": "Auffahrt",
"Entryway": "Zufahrt",
"FamilyRoom": "Familienzimmer",
"FrontYard": "Vorgarten",
"Garage": "Garage",
"GuestHouse": "Gästehaus",
"GuestRoom": "Gästezimmer",
"Hallway": "Flur",
"HomeCinema": "Heimkino",
"KidsRoom": "Kinderzimmer",
"Kitchen": "Küche",
"LaundryRoom": "Waschküche",
"Library": "Bibliothek",
"LivingRoom": "Wohnzimmer",
"LivingDining": "Wohn-Esszimmer",
"Loft": "Loft",
"Lounge": "Salon",
"MasterBedroom": "Hauptschlafzimmer",
"NannyRoom": "Kindermädchenzimmer",
"Office": "Büro",
"Outside": "Draußen",
"Patio": "Patio",
"Porch": "Veranda",
"Stairwell": "Treppenhaus",
"StorageRoom": "Lagerraum",
"Studio": "Studio",
"Shed": "Schuppen",
"Toilet": "Toilette",
"Terrace": "Terrasse",
"Upstairs": "Oben",
"Light": "Licht",
"Window": "Fenster",
"Door": "Tür",
"Motion": "Bewegungsmelder",
"Power": "Steckdose",
"Shutter": "Rolladen",
"Blind": "Jalousie",
"Fan": "Ventilator",
"AirCon": "Klimaanlage",
"Heating": "Heizung",
"Temperature": "Temperatur",
"Humidity": "Luftfeuchtigkeit"
}
@@ -0,0 +1,64 @@
{
"GroundFloor": "Ground Floor",
"FirstFloor": "First Floor",
"SecondFloor": "Second Floor",
"ThirdFloor": "Third Floor",
"FourthFloor": "Fourth Floor",
"Attic": "Attic",
"Balcony": "Balcony",
"Backyard": "Backyard",
"Basement": "Basement",
"Bathroom": "Bathroom",
"Bedroom": "Bedroom",
"Boiler": "Boiler Room",
"Wardrobe": "Wardrobe",
"Cellar": "Cellar",
"Corridor": "Corridor",
"Deck": "Deck",
"Dining": "Dining",
"Downstairs": "Downstairs",
"Driveway": "Driveway",
"Entryway": "Entryway",
"FamilyRoom": "Family Room",
"FrontYard": "Front Yard",
"Garage": "Garage",
"GuestHouse": "Guest House",
"GuestRoom": "Guest Room",
"Hallway": "Hallway",
"HomeCinema": "Home Cinema",
"KidsRoom": "Kids Room",
"Kitchen": "Kitchen",
"LaundryRoom": "Laundry Room",
"Library": "Library",
"LivingRoom": "Living Room",
"LivingDining": "Living & Dining Room",
"Loft": "Loft",
"Lounge": "Lounge",
"MasterBedroom": "Master Bedroom",
"NannyRoom": "Nanny Room",
"Office": "Office",
"Outside": "Outside",
"Patio": "Patio",
"Porch": "Porch",
"Stairwell": "Stairwell",
"StorageRoom": "Storage Room",
"Studio": "Studio",
"Shed": "Shed",
"Toilet": "Toilet",
"Terrace": "Terrace",
"Upstairs": "Upstairs",
"Light": "Light",
"Window": "Window",
"Door": "Door",
"Motion": "Motion Sensor",
"Power": "Power Outlet",
"Shutter": "Roller Shutter",
"Blind": "Blind",
"Fan": "Fan",
"AirCon": "Air Conditioner",
"Heating": "Heating",
"Temperature": "Temperature",
"Humidity": "Humidity"
}
@@ -0,0 +1,65 @@
{
"GroundFloor": "Planta Baja",
"FirstFloor": "Primera Planta",
"SecondFloor": "Segunda Planta",
"ThirdFloor": "Tercera Planta",
"FourthFloor": "Cuarta Planta",
"Attic": "Ático",
"Balcony": "Terraza",
"Backyard": "Patio trasero",
"Basement": "Sótano",
"Bathroom": "Baño",
"Bedroom": "Dormitorio",
"Boiler": "Cuarto de calderas",
"Wardrobe": "Vestidor",
"Cellar": "Bodega",
"Corridor": "Pasillo",
"Deck": "Cubierta",
"Dining": "Comedor",
"Downstairs": "Planta baja",
"Driveway": "Entrada de coches",
"Entryway": "Recibidor",
"FamilyRoom": "Habitación familiar",
"FrontYard": "Jardín",
"Garage": "Garaje",
"GuestHouse": "Casa de invitados",
"GuestRoom": "Habitación de invitados",
"Hallway": "Pasillo",
"HomeCinema": "Cine en casa",
"KidsRoom": "Habitación de los niños",
"Kitchen": "Cocina",
"LaundryRoom": "Despensa",
"Library": "Biblioteca",
"LivingRoom": "Salón",
"LivingDining": "Cocina americana",
"Loft": "Desván",
"Lounge": "Sala",
"MasterBedroom": "Dormitorio principal",
"NannyRoom": "Habitación de la niñera",
"Office": "Oficina",
"Outside": "Fuera",
"Patio": "Patio",
"Porch": "Porche",
"Stairwell": "Hueco de la escalera",
"StorageRoom": "Despensa",
"Studio": "Estudio",
"Shed": "Cobertizo",
"Toilet": "Baño",
"Terrace": "Terraza",
"Upstairs": "Piso de arriba",
"Light": "Luz",
"Window": "Ventana",
"Door": "Puerta",
"Motion": "Sensor de movimiento",
"Power": "Enchufe",
"Shutter": "Persiana",
"Blind": "Cortina",
"Fan": "Ventilador",
"AirCon": "Aire Acondicionado",
"Heating": "Calefacción",
"Temperature": "Temperatura",
"Humidity": "Humedad"
}
@@ -0,0 +1,65 @@
{
"GroundFloor": "Rez-de-chaussée",
"FirstFloor": "Premier étage",
"SecondFloor": "Deuxième étage",
"ThirdFloor": "Troisième étage",
"FourthFloor": "Quatrième étage",
"Attic": "Grenier",
"Balcony": "Balcon",
"Backyard": "Jardin",
"Basement": "Sous-sol",
"Bathroom": "Salle de bain",
"Bedroom": "Chambre",
"Boiler": "Chaudière",
"Wardrobe": "Armoire",
"Cellar": "Sous-sol",
"Corridor": "Couloir",
"Deck": "Terrasse en bois",
"Dining": "Salle à manger",
"Downstairs": "En bas",
"Driveway": "Allée",
"Entryway": "Entrée",
"FamilyRoom": "Salon",
"FrontYard": "Jardin",
"Garage": "Garage",
"GuestHouse": "Maison dhôte",
"GuestRoom": "Chambre damis",
"Hallway": "Couloir",
"HomeCinema": "Home cinéma",
"KidsRoom": "Salle de jeux",
"Kitchen": "Cuisine",
"LaundryRoom": "Buanderie",
"Library": "Bibliothèque",
"LivingRoom": "Salon",
"LivingDining": "Cuisine à laméricaine",
"Loft": "Loft",
"Lounge": "Salon",
"MasterBedroom": "Chambre parentale",
"NannyRoom": "Chambre de bonne",
"Office": "Bureau",
"Outside": "Dehors",
"Patio": "Patio",
"Porch": "Véranda",
"Stairwell": "Cage descalier",
"StorageRoom": "Débarras",
"Studio": "Studio",
"Shed": "Cabane",
"Toilet": "Toilettes",
"Terrace": "Terrasse",
"Upstairs": "En haut",
"Light": "Lumière",
"Window": "Fenêtre",
"Door": "Porte",
"Motion": "Mouvement",
"Power": "Courant",
"Shutter": "Volet",
"Blind": "Store",
"Fan": "Ventilateur",
"AirCon": "Air conditionné",
"Heating": "Chauffage",
"Temperature": "Temperature",
"Humidity": "Humidité"
}
@@ -0,0 +1,65 @@
{
"GroundFloor": "Piano Terra",
"FirstFloor": "Primo Piano",
"SecondFloor": "Secondo Piano",
"ThirdFloor": "Terzo Piano",
"FourthFloor": "Quarto Piano",
"Attic": "Attico",
"Balcony": "Balcone",
"Backyard": "Cortile sul retro",
"Basement": "Cantina",
"Bathroom": "Stanza da Bagno",
"Bedroom": "Camera da Letto",
"Boiler": "Caldaia",
"Wardrobe": "Armadio",
"Cellar": "Scantinato",
"Corridor": "Corridio",
"Deck": "Deck",
"Dining": "Sala da Pranzo",
"Downstairs": "Piano di Sotto",
"Driveway": "Vialetto",
"Entryway": "Ingresso",
"FamilyRoom": "Soggiorno",
"FrontYard": "Giardino Frontale",
"Garage": "Garage",
"GuestHouse": "Casa per gli Ospiti",
"GuestRoom": "Stanza per gli Ospiti",
"Hallway": "Corridoio",
"HomeCinema": "Home Cinema",
"KidsRoom": "Stanza dei Bambini",
"Kitchen": "Cucina",
"LaundryRoom": "Lavanderia",
"Library": "Biblioteca",
"LivingRoom": "Sala",
"LivingDining": "Cucina Open Space",
"Loft": "Loft",
"Lounge": "Lounge",
"MasterBedroom": "Camera da letto principale",
"NannyRoom": "Stanza della governante",
"Office": "Ufficio",
"Outside": "Esterno",
"Patio": "Patio",
"Porch": "Portico",
"Stairwell": "Scalinata",
"StorageRoom": "Dispensa",
"Studio": "Studio",
"Shed": "Capanno",
"Toilet": "Bagno",
"Terrace": "Terrazza",
"Upstairs": "Piano di Sopra",
"Light": "Luce",
"Window": "Finestra",
"Door": "Porta",
"Motion": "Sensore di movimento",
"Power": "Elettricità",
"Shutter": "Persiana",
"Blind": "Tenda",
"Fan": "Ventilatore",
"AirCon": "Aria Condizionata",
"Heating": "Riscaldamento",
"Temperature": "Temperatura",
"Humidity": "Umidità"
}
@@ -0,0 +1,65 @@
{
"GroundFloor": "1층",
"FirstFloor": "2층",
"SecondFloor": "3층",
"ThirdFloor": "4층",
"FourthFloor": "5층",
"Attic": "다락방",
"Balcony": "발코니",
"Backyard": "뒷마당",
"Basement": "지하실",
"Bathroom": "욕실",
"Bedroom": "침실",
"Boiler": "보일러실",
"Wardrobe": "옷장",
"Cellar": "지하실",
"Corridor": "복도",
"Deck": "갑판",
"Dining": "식당",
"Downstairs": "아래층",
"Driveway": "입구",
"Entryway": "정문",
"FamilyRoom": "패밀리 룸",
"FrontYard": "앞마당",
"Garage": "차고",
"GuestHouse": "게스트 하우스",
"GuestRoom": "게스트실",
"Hallway": "복도",
"HomeCinema": "홈 시네마",
"KidsRoom": "어린이방",
"Kitchen": "부엌",
"LaundryRoom": "세탁실",
"Library": "도서관",
"LivingRoom": "거실",
"LivingDining": "거실",
"Loft": "옥탑",
"Lounge": "라운지",
"MasterBedroom": "안방",
"NannyRoom": "보모실",
"Office": "사무실",
"Outside": "외부",
"Patio": "패티오",
"Porch": "폴치",
"Stairwell": "계단",
"StorageRoom": "창고",
"Studio": "스튜디오",
"Shed": "창고",
"Toilet": "화장실",
"Terrace": "테라스",
"Upstairs": "위층",
"Light": "라이트",
"Window": "창",
"Door": "문",
"Motion": "모션 센서",
"Power": "전원",
"Shutter": "셔터",
"Blind": "블라인드",
"Fan": "팬",
"AirCon": "에어컨",
"Heating": "난방",
"Temperature": "온동",
"Humidity": "습도"
}
@@ -0,0 +1,64 @@
{
"GroundFloor": "Parter",
"FirstFloor": "Pierwsze Piętro",
"SecondFloor": "Drugie Piętro",
"ThirdFloor": "Trzecie Piętro",
"FourthFloor": "Czwarte Piętro",
"Attic": "Poddasze",
"Balcony": "Balkon",
"Backyard": "Ogród",
"Basement": "Piwnica",
"Bathroom": "Łazienka",
"Bedroom": "Sypialnia",
"Boiler": "Kotłownia",
"Wardrobe": "Garderoba",
"Cellar": "Piwnica",
"Corridor": "Korytarz",
"Deck": "Pokład",
"Dining": "Jadalnia",
"Downstairs": "Na dole",
"Driveway": "Podjazd",
"Entryway": "Wejście",
"FamilyRoom": "Pokój rodzinny",
"FrontYard": "Ogródek",
"Garage": "Garaż",
"GuestHouse": "Dom gościnny",
"GuestRoom": "Pokój gościnny",
"Hallway": "Hol",
"HomeCinema": "Kino Domowe",
"KidsRoom": "Pokój Dziecięcy",
"Kitchen": "Kuchnia",
"LaundryRoom": "Pralnia",
"Library": "Biblioteka",
"LivingRoom": "Pokój Dzienny",
"LivingDining": "Pokój Dzienny z Jadalnią",
"Loft": "Loft",
"Lounge": "Loża",
"MasterBedroom": "Główna Sypialnia",
"NannyRoom": "Pokój Niani",
"Office": "Biuro",
"Outside": "Posesja",
"Patio": "Patio",
"Porch": "Ganek",
"Stairwell": "Klatka Schodowa",
"StorageRoom": "Przechowalnia",
"Studio": "Studio",
"Shed": "Szopa",
"Toilet": "Toaleta",
"Terrace": "Taras",
"Upstairs": "Na górze",
"Light": "Światło",
"Window": "Okno",
"Door": "Drzwi",
"Motion": "Czujnik ruchu",
"Shutter": "Roleta",
"Blind": "Żaluzja",
"Fan": "Wiatrak",
"AirCon": "Klimatyzator",
"Heating": "Ogrzewanie",
"Temperature": "Temperatura",
"Humidity": "Wilgotność",
"Power": "Gniazdko"
}
@@ -0,0 +1,64 @@
{
"GroundFloor": "Первый этаж",
"FirstFloor": "Второй этаж",
"SecondFloor": "Третий этаж",
"ThirdFloor": "Четвертый этаж",
"FourthFloor": "Пятый этаж",
"Attic": "Чердак",
"Balcony": "Балкон",
"Backyard": "Задний двор",
"Basement": "Подвал",
"Bathroom": "Ванная комната",
"Bedroom": "Спальня",
"Boiler": "Бойлерная",
"Wardrobe": "Гардероб",
"Cellar": "Подвал",
"Corridor": "Коридор",
"Deck": "Палуба",
"Dining": "Столовая",
"Downstairs": "Вниз",
"Driveway": "Заезд",
"Entryway": "Подъезд",
"FamilyRoom": "Гостиная",
"FrontYard": "Передний двор",
"Garage": "Гараж",
"GuestHouse": "Гостевой дом",
"GuestRoom": "Гостевая комната",
"Hallway": "Прихожая",
"HomeCinema": "Домашний кинотеатр",
"KidsRoom": "Детская комната",
"Kitchen": "Кухня",
"LaundryRoom": "Прачечная",
"Library": "Библиотека",
"LivingRoom": "Гостиная",
"LivingDining": "Гостиная и столовая",
"Loft": "Лофт",
"Lounge": "Комната отдыха",
"MasterBedroom": "Главная спальня",
"NannyRoom": "Комната няни",
"Office": "Офис",
"Outside": "За домом",
"Patio": "Внутренний дворик",
"Porch": "Крыльцо",
"Stairwell": "Лестничная площадка",
"StorageRoom": "Кладовка",
"Studio": "Студия",
"Shed": "Сарай",
"Toilet": "Туалет",
"Terrace": "Терраса",
"Upstairs": "Подъем",
"Light": "Освещение",
"Window": "Окно",
"Door": "Дверь",
"Motion": "Датчик движения",
"Power": "Розетка",
"Shutter": "Шторы",
"Blind": "Жалюзи",
"Fan": "Вентилятор",
"AirCon": "Кондиционер",
"Heating": "Обогрев",
"Temperature": "Температура",
"Humidity": "Влажность"
}
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>HomeBuilder</title>
<link rel="shortcut icon" href="favicon.ico" />
</head>
<body>
<div id="app"></div>
<script src="dist/build.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
{
"name": "homebuilder",
"description": "openHAB HomeBuilder - boilerplate for the Items, sitemap and more",
"version": "1.0.0",
"authors": [
"Kuba Wolanin <hi@kubawolanin.com> (github: kubawolanin)"
],
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
},
"dependencies": {
"ascii-table": "0.0.9",
"bootstrap-vue": "^0.18.0",
"daemonite-material": "^4.0.0-alpha.6",
"lodash": "^4.17.4",
"underscore.string": "^3.3.4",
"v-clipboard": "^1.0.4",
"vue": "^2.3.3",
"vue-form-generator": "^2.0.0",
"vue-i18n": "^7.1.1",
"vue-multiselect": "^2.0.2",
"vue-resource": "^1.3.4"
},
"devDependencies": {
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-preset-env": "^1.5.1",
"cross-env": "^3.0.0",
"css-loader": "^0.25.0",
"file-loader": "^0.9.0",
"node-sass": "^4.5.0",
"sass-loader": "^5.0.1",
"style-loader": "^0.18.2",
"vue-affix": "^0.2.1",
"vue-loader": "^12.1.0",
"vue-template-compiler": "^2.3.3",
"webpack": "^2.6.1",
"webpack-dev-server": "^2.4.5"
}
}
@@ -0,0 +1,21 @@
<template>
<div>
<app-nav></app-nav>
<div class="container" id="app">
<app-form></app-form>
</div>
</div>
</template>
<script>
import AppNav from './AppNav.vue'
import AppForm from './AppForm.vue'
export default {
name: 'app',
components: {
AppNav,
AppForm
}
}
</script>
@@ -0,0 +1,201 @@
<template>
<div class='row'>
<div class='col-sm-6' id="form">
<vue-form-generator :schema='schema' :model='model' :options='formOptions'>
</vue-form-generator>
<pre class='language-json hidden-xs-up'><code>{{ model }}</code></pre>
</div>
<div class='col-sm-6'>
<affix id="preview" relative-element-selector="#form" :offset="{ top: 65, bottom: 0 }">
<collapse v-if="model && model.filesGenerated.includes('items')" :uid="'items'" :heading="'Items'" :content='model.itemsType === "text" ? generateItems(model) : generateItemsJson(model)'></collapse>
<collapse v-if="model && model.filesGenerated.includes('sitemap')" :uid="'sitemap'" :heading="'Sitemap'" :content='generateSitemap(model)'></collapse>
<collapse v-if="model && model.filesGenerated.includes('habpanel')" :uid="'habpanel'" :heading="'HABPanel Dashboard'" :content='generateDashboard(model)'></collapse>
</affix>
</div>
</div>
</template>
<style lang="scss" src="./scss/app.scss">
</style>
<script>
import * as _ from 'lodash'
import * as s from 'underscore.string'
import Vue from 'vue'
import Collapse from './Collapse.vue'
import { component as VueFormGenerator } from 'vue-form-generator'
import { floors, rooms, objects, languages, OBJECTS_SUFFIX } from './definitions'
import * as schema from './formSchema'
import { generateItems } from './textItems'
import { generateItemsJson } from './restItems'
import { generateDashboard } from './habpanel'
import { sitemapName, generateSitemap } from './sitemap'
export default {
components: {
VueFormGenerator,
Collapse
},
data() {
return {
model: {
language: 'en-UK',
homeName: 'Our Home',
filesGenerated: ['items', 'sitemap'],
itemsType: 'text',
itemsChannel: true,
itemsIcons: true,
floorsCount: 1,
GroundFloor: []
},
schema: {
groups: [
{ legend: '', fields: schema.basicFields },
{ legend: 'Floors', fields: schema.floorsFields },
{ legend: 'Rooms', fields: schema.roomsFields },
{ legend: 'Objects', fields: schema.objectsFields },
{ legend: '', fields: schema.settingsFields }
],
},
formOptions: {
validateAfterLoad: true,
validateAfterChanged: true,
fieldIdPrefix: 'user-'
}
}
},
methods: {
/**
* Generates textual Items
*/
generateItems,
/**
* Generates JSON array of Items,
* understandable by REST API
*/
generateItemsJson,
/**
* Generates textual Sitemap
*/
generateSitemap,
/**
* Generates HABPanel dashboard JSON config
*/
generateDashboard,
/**
* Gets i18n configuration from ESH service
*/
getLocale: function() {
const DEFAULT_LOCALE = 'en-UK';
this.$http
.get('services/org.eclipse.smarthome.core.i18nprovider/config')
.then(response => {
const body = response.body;
let selectedLang = DEFAULT_LOCALE;
if (body.language && body.region) {
let langId = body.language + '-' + body.region;
let lang = _.find(languages, { id: langId });
selectedLang = lang ? langId : DEFAULT_LOCALE;
}
this.$data.model.language = selectedLang;
this.fetchTranslations(selectedLang);
})
.catch(reason => {
this.$data.model.language = DEFAULT_LOCALE;
this.fetchTranslations(DEFAULT_LOCALE);
});
},
/**
* Loads translation file from the `/i18n/` folder
* and assigns new `name` properties to the definitions.
*/
fetchTranslations: function(language) {
let root = window.location.href.replace('/index.html', '');
this.$http
.get(root + '/i18n/' + language + '.json')
.then(response => {
this.$i18n.locale = language;
this.$i18n.setLocaleMessage(language, response.body);
let stack = [...rooms, ...objects];
let roomsModel = _.chain(this.$data.model)
.pickBy((value, key) => _.endsWith(key, OBJECTS_SUFFIX))
.value();
floors.forEach(function(item) {
stack = [...stack, item];
if (this.$data.model[item.value]) {
stack = [...stack, ...this.$data.model[item.value]];
}
}.bind(this));
if (!_.isEmpty(roomsModel)) {
_.forOwn(roomsModel, (room) => { stack = [...stack, ...room] });
}
stack.forEach(item => {
if (!item.custom) {
item.name = this.$i18n.t(item.value);
}
});
this.$forceUpdate();
});
},
resizeAffix(event) {
let bodyWidth = document.body && document.body.clientWidth;
let formEl = document.getElementById('form');
let previewEl = document.getElementById('preview');
let formWidth = formEl && form.clientWidth;
let hidden = 'hidden-xs-up';
previewEl.style.width = formWidth + 'px';
if (bodyWidth <= 599) {
previewEl.classList.add(hidden);
document.querySelector('.navbar-toggler-icon').classList.remove(hidden);
document.querySelector('.navbar-close-icon').classList.add(hidden);
} else {
previewEl.classList.remove(hidden);
}
},
},
http: {
root: window.location.origin + '/rest/'
},
mounted: function() {
this.getLocale();
this.$nextTick(function() {
window.addEventListener('resize', this.resizeAffix);
this.resizeAffix();
})
},
watch: {
locale(val) {
this.$i18n.locale = val
}
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeAffix);
}
}
</script>
@@ -0,0 +1,41 @@
<template>
<nav class="navbar fixed-top">
<a class="navbar-brand mr-auto" href="#">HomeBuilder</a>
<button @click="togglePreview" type="button" aria-label="Toggle preview" class="navbar-toggler navbar-toggler-right">
<span class="navbar-toggler-icon"></span>
<span class="navbar-close-icon hidden-xs-up">&times;</span>
</button>
</nav>
</template>
<style lang="scss">
.navbar {
background: #E64A19;
color: #fff;
}
.navbar-toggler {
display: none;
}
.navbar-close-icon {
color: #200f04;
font-size: 36px;
padding-left: 1px;
margin-top: -8px;
}
</style>
<script>
export default {
methods: {
togglePreview() {
document.getElementById('preview').classList.toggle('hidden-xs-up');
document.querySelector('.navbar-close-icon').classList.toggle('hidden-xs-up');
document.querySelector('.navbar-toggler-icon').classList.toggle('hidden-xs-up');
}
}
}
</script>
@@ -0,0 +1,78 @@
<template>
<div class="pb-1">
<b-btn block v-b-toggle="uid" variant="info" v-html="heading"></b-btn>
<b-collapse :id="uid" accordion="preview">
<b-card>
<button @click="createItems" v-if="uid === 'items' && content instanceof Array" class="btn-clipboard btn-create-items">{{ createText }}</button>
<button @click="copy" class="btn-clipboard" title="Copy to clipboard">{{ copyText }}</button>
<span class="pb-1" v-if="uid === 'items' && content instanceof Array">
Number of Items: {{content.length}}
</span>
<span class="pb-1" v-if="uid === 'habpanel'">
Paste the content in
<a href="../../habpanel/index.html#/settings/localconfig" target="_blank">this config page</a>.
</span>
<pre v-html='content'></pre>
</b-card>
</b-collapse>
</div>
</template>
<script>
import Vue from 'vue'
import Clipboard from 'v-clipboard'
Vue.use(Clipboard);
export default {
props: ['uid', 'heading', 'content'],
data: () => ({
copyText: 'Copy',
createText: 'Create Items'
}),
http: {
root: window.location.origin + '/rest/'
},
methods: {
copy() {
let content = this.content;
if (typeof content === 'object') {
content = JSON.stringify(this.content, null, 2);
} else {
content = content
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}
this.$clipboard(content);
this.copyText = 'Copied!';
setTimeout(() => {
this.copyText = 'Copy';
}, 500);
},
createItems() {
let content = this.content;
if (typeof content === 'object') {
this.$http
.put('items', content)
.then(response => {
if (response.status === 200) {
this.createText = 'Done!';
} else {
this.createText = 'Error!';
}
setTimeout(() => {
this.createText = 'Create Items';
}, 500);
});
}
}
}
}
</script>
@@ -0,0 +1,91 @@
/**
* i18n definitions
*/
export var languages = [
{ name: 'English', id: 'en-UK' },
{ name: 'French', id: 'fr-FR' },
{ name: 'German', id: 'de-DE' },
{ name: 'Italian', id: 'it-IT' },
{ name: 'Korean', id: 'ko-KR' },
{ name: 'Polish', id: 'pl-PL' },
{ name: 'Russian', id: 'ru-RU' },
{ name: 'Spanish', id: 'es-ES' }
];
/**
* Structure definitions
*/
export var floors = [
{ abbr: 'GF', value: 'GroundFloor', icon: 'groundfloor' },
{ abbr: 'FF', value: 'FirstFloor', icon: 'firstfloor' },
{ abbr: 'F2', value: 'SecondFloor', icon: 'attic' },
{ abbr: 'F3', value: 'ThirdFloor', icon: 'attic' },
{ abbr: 'F4', value: 'FourthFloor', icon: 'attic' }
];
export var rooms = [
{ value: 'Attic', icon: 'attic' },
{ value: 'Balcony', icon: '' },
{ value: 'Backyard', icon: 'lawnmower' },
{ value: 'Basement', icon: 'cellar' },
{ value: 'Bathroom', icon: 'bath' },
{ value: 'Bedroom', icon: 'bedroom' },
{ value: 'Boiler', icon: 'gas' },
{ value: 'Wardrobe', icon: 'wardrobe' },
{ value: 'Cellar', icon: 'cellar' },
{ value: 'Corridor', icon: 'corridor' },
{ value: 'Deck', icon: '' },
{ value: 'Dining', icon: '' },
{ value: 'Downstairs', icon: 'cellar' },
{ value: 'Driveway', icon: '' },
{ value: 'Entryway', icon: 'frontdoor' },
{ value: 'FamilyRoom', icon: 'parents_2_4' },
{ value: 'FrontYard', icon: 'lawnmower' },
{ value: 'Garage', icon: 'garage' },
{ value: 'GuestHouse', icon: 'house' },
{ value: 'GuestRoom', icon: 'parents_4_3' },
{ value: 'Hallway', icon: 'corridor' },
{ value: 'HomeCinema', icon: 'screen' },
{ value: 'KidsRoom', icon: 'girl_3' },
{ value: 'Kitchen', icon: 'kitchen' },
{ value: 'LaundryRoom', icon: 'washingmachine' },
{ value: 'Library', icon: 'office' },
{ value: 'LivingRoom', icon: 'sofa' },
{ value: 'LivingDining', icon: 'sofa' },
{ value: 'Loft', icon: 'attic' },
{ value: 'Lounge', icon: 'sofa' },
{ value: 'MasterBedroom', icon: 'bedroom_red' },
{ value: 'NannyRoom', icon: 'woman_1' },
{ value: 'Office', icon: 'office' },
{ value: 'Outside', icon: 'garden' },
{ value: 'Patio', icon: 'terrace' },
{ value: 'Porch', icon: 'group' },
{ value: 'Stairwell', icon: 'qualityofservice' },
{ value: 'StorageRoom', icon: 'suitcase' },
{ value: 'Studio', icon: 'pantry' },
{ value: 'Shed', icon: 'greenhouse' },
{ value: 'Toilet', icon: 'toilet' },
{ value: 'Terrace', icon: 'terrace' },
{ value: 'Upstairs', icon: 'firstfloor' }
];
/**
* Collection of objects (sensors, smart devices etc.) controllable by openHAB.
*/
export var objects = [
{ value: 'Light', icon: 'light', type: 'Switch:OR(ON, OFF)', unit: '[(%d)]' },
{ value: 'Window', icon: 'window', type: 'Contact:OR(OPEN, CLOSED)', unit: '[MAP(en.map):%s]' },
{ value: 'Door', icon: 'door', type: 'Contact:OR(OPEN, CLOSED)', unit: '[MAP(en.map):%s]' },
{ value: 'Motion', icon: 'motion', type: 'Switch:OR(ON, OFF)', unit: '[(%d)]' },
{ value: 'Power', icon: 'poweroutlet', type: 'Switch:OR(ON, OFF)', unit: '[(%d)]' },
{ value: 'Shutter', icon: 'rollershutter', type: 'Rollershutter:OR(UP, DOWN)', unit: '[(%d)]' },
{ value: 'Blind', icon: 'blinds', type: 'Dimmer', unit: '[%d %%]' },
{ value: 'Fan', icon: 'fan_ceiling', type: 'Switch:OR(ON, OFF)', unit: '[(%d)]' },
{ value: 'AirCon', icon: 'snow', type: 'Switch:OR(ON, OFF)', unit: '[(%d)]' },
{ value: 'Heating', icon: 'heating', type: 'Number:AVG', unit: '[%.1f °C]' },
{ value: 'Temperature', icon: 'temperature', type: 'Number:AVG', unit: '[%.1f °C]' },
{ value: 'Humidity', icon: 'humidity', type: 'Number:AVG', unit: '[%d %%]' },
];
export const OBJECTS_SUFFIX = '_objects';
@@ -0,0 +1,169 @@
<template>
<multiselect
:id="selectOptions.id"
:options="options"
:value="value"
:multiple="selectOptions.multiple"
:track-by="selectOptions.trackBy || null"
:label="selectOptions.label || null"
:searchable="selectOptions.searchable"
:clear-on-select="selectOptions.clearOnSelect"
:hide-selected="selectOptions.hideSelected"
:placeholder="schema.placeholder"
:allow-empty="selectOptions.allowEmpty"
:reset-after="selectOptions.resetAfter"
:close-on-select="selectOptions.closeOnSelect"
:custom-label="customLabel"
:taggable="selectOptions.taggable"
:tag-placeholder="selectOptions.tagPlaceholder"
:max="schema.max || null"
:options-limit="selectOptions.optionsLimit"
:group-values="selectOptions.groupValues"
:group-label="selectOptions.groupLabel"
:block-keys="selectOptions.blockKeys"
:internal-search="selectOptions.internalSearch"
:select-label="selectOptions.selectLabel"
:selected-label="selectOptions.selectedLabel"
:deselect-label="selectOptions.deselectLabel"
:show-labels="selectOptions.showLabels"
:limit="selectOptions.limit"
:limit-text="selectOptions.limitText"
:loading="selectOptions.loading"
:disabled="disabled"
:max-height="selectOptions.maxHeight"
:show-pointer="selectOptions.showPointer"
@input="updateSelected"
@select="onSelect"
@remove="onRemove"
@search-change="onSearchChange"
@tag="addTag"
@open="onOpen"
@close="onClose"
:option-height="selectOptions.optionHeight"
>
<template slot="tag" scope="props">
<span class="multiselect__tag">
<img :src="generateIcon(props.option.icon)" />
<span>{{ props.option.name }}</span>
<i aria-hidden="true"
tabindex="1"
@click="props.remove(props.option)"
class="multiselect__tag-icon"></i>
</span>
</template>
<template slot="option" scope="props">
<img class="option__image" :src="generateIcon(props.option.icon)" />
<div class="option__desc">
<span class="option__title">{{ props.option.name }}</span>
<span class="option__small text-muted">{{ props.option.type || ''}}</span>
</div>
</template>
</multiselect>
</template>
<style lang="scss">
.option__image,
.option__desc {
display: inline-block;
vertical-align: middle;
.option__small {
display: block;
font-weight: normal;
font-size: .725rem;
}
}
.multiselect__element {
margin: 0;
}
.multiselect__option {
padding: 5px;
}
.multiselect__tag {
img {
width: 16px;
height: 16px;
&[src*="none.svg"] {
display: none;
}
}
}
.option__image {
width: 32px;
height: 32px;
}
</style>
<script>
import { abstractField } from 'vue-form-generator';
export default {
mixins: [abstractField],
computed: {
selectOptions() {
return this.schema.selectOptions || {};
},
options() {
let values = this.schema.values;
if (typeof(values) === 'function') {
return values.apply(this, [this.model, this.schema]);
} else {
return values;
}
},
customLabel(){
if (typeof this.schema.selectOptions !== 'undefined' && typeof this.schema.selectOptions.customLabel !== 'undefined' && typeof this.schema.selectOptions.customLabel === 'function') {
return this.schema.selectOptions.customLabel;
} else {
//this will let the multiselect library use the default behavior if customLabel is not specified
return undefined;
}
}
},
methods: {
updateSelected(value/*, id*/) {
this.value = value;
},
generateIcon(icon) {
return window.location.origin + '/icon/' + (icon || 'none') + '.svg'
},
addTag(newTag, id) {
let onNewTag = this.selectOptions.onNewTag;
if (typeof(onNewTag) == 'function') {
onNewTag(newTag, id, this.options, this.value);
}
},
onSearchChange(searchQuery, id) {
let onSearch = this.selectOptions.onSearch;
if (typeof(onSearch) == 'function') {
onSearch(searchQuery, id, this.options);
}
},
onSelect(/*selectedOption, id*/) {
// console.log("onSelect", selectedOption, id);
},
onRemove(/*removedOption, id*/) {
// console.log("onRemove", removedOption, id);
},
onOpen(/*id*/) {
// console.log("onOpen", id);
},
onClose(/*value, id*/) {
// console.log("onClose", value, id);
}
},
created() {
// Check if the component is loaded globally
if (!this.$root.$options.components['multiselect']) {
console.error("'vue-multiselect' is missing. Please download from https://github.com/monterail/vue-multiselect and register the component globally!");
}
}
};
</script>
@@ -0,0 +1,330 @@
import * as _ from 'lodash'
import * as s from 'underscore.string'
import { validators } from 'vue-form-generator'
import { languages, floors, rooms, objects, OBJECTS_SUFFIX } from './definitions'
/**
* Invoked when language select has changed its value.
*
* @param {Object} model
* @param {Object} newVal
*/
function languageChanged(model, newVal) {
this.$parent.$parent.fetchTranslations(newVal);
}
/**
* Creates a custom room entry
*
* @param {string} newTag
* @param {string} id
* @param {Object} options
* @param {string} value
*/
function newRoomTag(newTag, id, options, value) {
const tag = {
name: newTag,
icon: 'none',
custom: true,
value: s(newTag)
.trim()
.toLowerCase()
.cleanDiacritics()
.classify()
.value()
}
rooms.push(tag);
value.push(tag);
};
/**
* Returns a group function for a given
* Item's type
* @param {*} type
*/
function getGroupFunc(type) {
let func = '';
switch (type) {
case 'Switch':
func = 'OR(ON, OFF)';
break;
case 'Contact':
func = 'OR(OPEN, CLOSED)';
break;
case 'Rollershutter':
func = 'OR(UP, DOWN)';
break;
case 'Number':
func = 'AVG';
break;
default:
break;
}
return func;
}
/**
* Creates a custom object entry
*
* @param {string} newTag
* @param {string} id
* @param {Object} options
* @param {string} value
*/
function newObjectTag(newTag, id, options, value) {
let split = newTag.split(':');
let type = split.length > 1 ? _.first(split).trim() : 'Switch';
let name = split.length > 1 ? split[1].trim() : newTag;
let groupFunc = getGroupFunc(type);
const tag = {
name: name,
icon: 'none',
type: type + ':' + groupFunc,
unit: '[(%d)]',
custom: true,
value: s(name)
.trim()
.toLowerCase()
.cleanDiacritics()
.classify()
.value()
}
objects.push(tag);
value.push(tag);
};
/**
* Is being executed when
* collection of rooms in floor multiselect field
* has changed.
*
* If there's a new `room` in collection,
* a new dynamic field is added to the floor object, e.g.
* `"GroundFloor": [ { name: 'Bedroom', value: 'Bedroom', icon: 'bed' }]`
*
* If an entry was removed from the collection,
* a dynamic field is removed accordingly.
*
* @param {Object} model
* @param {Array} newVal
* @param {Array} oldVal
* @param {Object} field
*/
function roomsChanged(model, newVal, oldVal, field) {
let objectsFields = _.find(this.$options.parent.groups, { legend: 'Objects' }).fields;
let floor = field.model;
let oldList = oldVal ? _.map(oldVal, 'value') : [];
let newList = _.map(newVal, 'value');
let lastRemoved = _.first(_.difference(oldList, newList));
let lastItem = _.last(newList);
let roomName = '';
if (lastItem && !lastRemoved) {
let room = _.find(rooms, { value: lastItem });
roomName = floor + '_' + room.value + OBJECTS_SUFFIX;
objectsFields.push({
type: 'multiselect',
label: (room.name || room.value) + ' (' + _.find(floors, { value: floor }).name + ')',
styleClasses: 'rooms-list',
model: roomName,
placeholder: 'Type to search object',
selectOptions: {
multiple: true,
hideSelected: true,
closeOnSelect: false,
selectLabel: '',
trackBy: 'value',
label: 'name',
searchable: true,
taggable: true,
onNewTag: newObjectTag
},
values: objects
});
}
if (lastRemoved) {
roomName = floor + '_' + lastRemoved + OBJECTS_SUFFIX;
_.remove(objectsFields, { model: roomName });
delete model[roomName];
}
}
/**
* Schema describing the basic form
* generated by vue-form-generator
*/
export var basicFields = [{
type: 'select',
model: 'language',
label: 'Please select your language',
values: function() {
return languages;
},
selectOptions: {
hideNoneSelectedText: true
},
onChanged: languageChanged
},
{
type: 'input',
inputType: 'text',
label: 'Home Setup Name',
model: 'homeName',
readonly: false,
disabled: false,
placeholder: 'Home name',
validator: validators.string
}
];
export var floorsFields = [{
type: 'radios',
model: 'floorsCount',
label: 'Number of floors',
styleClasses: 'floors-number',
values: [1, 2, 3, 4, 5],
onChanged: function(model, newVal, oldVal, field) {
let roomsFields = _.find(this.$options.parent.groups, { legend: 'Rooms' }).fields;
if (newVal <= 5 && newVal > oldVal) {
for (var i = oldVal; i < newVal; i++) {
let floor = floors[i];
let floorName = floor.value;
let fieldExists = _.find(roomsFields, (field) => field.model === floorName);
if (!fieldExists) {
roomsFields.push(roomsSelect(floorName, floor.name || floor.value));
}
}
}
if (newVal < oldVal) {
for (var j = oldVal; j > newVal; j--) {
let floorName = floors[j - 1].value;
_.remove(roomsFields, { model: floorName });
delete model[floorName];
}
}
}
}];
export var settingsFields = [{
type: 'checklist',
model: 'filesGenerated',
label: 'What would you like to generate?',
multi: true,
listBox: true,
multiSelect: true,
values: [
{ name: 'Items', value: 'items' },
{ name: 'Sitemap', value: 'sitemap' },
{ name: 'Dashboard', value: 'habpanel' }
]
},
{
type: 'radios',
label: 'How would you like to store the Items?',
model: 'itemsType',
visible(model) {
return model && model.filesGenerated.includes('items');
},
values: [
{ name: 'Textual Configuration Files', value: 'text' },
{ name: 'Internal Database', value: 'rest' }
]
},
{
type: 'switch',
label: 'Append channel to the non-Group items',
model: 'itemsChannel',
default: true,
textOn: 'Yes',
textOff: 'No',
valueOn: true,
valueOff: false,
visible(model) {
return model &&
model.filesGenerated.includes('items') &&
model.itemsType === 'text';
}
},
{
type: 'switch',
label: 'Include icons',
model: 'itemsIcons',
default: true,
textOn: 'Yes',
textOff: 'No',
valueOn: true,
valueOff: false,
visible(model) {
return model &&
(model.filesGenerated.includes('items') || model.filesGenerated.includes('sitemap'));
}
},
{
type: 'switch',
label: 'Include tags',
model: 'itemsTags',
default: false,
textOn: 'Yes',
textOff: 'No',
valueOn: true,
valueOff: false,
visible(model) {
return model && model.filesGenerated.includes('items');
}
}
];
/**
* Generates a multiselect input with
* rooms for specific floor (model)
*
* @param {string} model
* @param {string} label
*/
function roomsSelect(model, label) {
return {
type: 'multiselect',
label: label,
styleClasses: 'rooms-list',
model: model,
values: rooms,
placeholder: 'Type to search or add room',
selectOptions: {
multiple: true,
trackBy: 'value',
label: 'name',
selectLabel: '',
searchable: true,
taggable: true,
closeOnSelect: false,
hideSelected: true,
tagPlaceholder: 'Add this as a new room',
onNewTag: newRoomTag
},
onChanged: roomsChanged
};
}
/**
* Field group schema for the rooms
*/
export var roomsFields = [
roomsSelect('GroundFloor', 'Ground Floor')
];
/**
* Field group schema for the objects
*/
export var objectsFields = [];
@@ -0,0 +1,114 @@
import * as _ from 'lodash'
import { floors, objects, OBJECTS_SUFFIX } from './definitions'
import { getChosenObjects } from './restItems'
/**
* Generates a HABPanel's widget object
* @param {*} options
*/
function makeWidget(options) {
let row = options.row > 0 ? options.row * 2 : 0;
let widget = {
item: options.item,
name: options.name,
sizeX: 2,
sizeY: 2,
type: options.type,
row: row,
col: options.col,
font_size: '24',
useserverformat: true
};
if (options.type === 'switch') {
widget = _.extend({}, widget, {
iconset: 'eclipse-smarthome-classic',
icon: options.icon,
icon_size: 64
});
} else {
widget = _.extend({}, widget, {
backdrop_iconset: 'eclipse-smarthome-classic',
backdrop_icon: options.icon,
backdrop_center: true
});
}
return widget;
}
function getWidgetType(type) {
let widget = '';
switch (type) {
case 'Dimmer':
widget = 'slider';
break;
case 'Switch':
widget = 'switch';
break;
default:
widget ='dummy';
break;
}
return widget;
}
/**
* Generates an array widgets for specific Dashboard
* @param {*} object
* @param {*} model
*/
function makeWidgets(object, model) {
let widgets = [];
for (var i = 0; i < model.floorsCount; i++) {
var floor = floors[i];
if (floor && floor.value && !_.isUndefined(model[floor.value])) {
model[floor.value].forEach(function(room) {
let roomObjects = floor.value + '_' + room.value + OBJECTS_SUFFIX;
let objectCollection = model[roomObjects] || [];
let obj = objectCollection.find(o => o.value === object.value);
if (obj) {
widgets.push(makeWidget({
item: (model.floorsCount > 1 ? floor.abbr + '_' : '') + room.value + '_' + obj.value,
name: room.name,
type: getWidgetType(_.first(object.type.split(':'))),
row: _.chunk(widgets, 6).length - 1,
col: (widgets.length * 2) % 12,
icon: object.icon
}));
}
});
}
}
return widgets;
};
/**
* Generates a full HABPanel dashboard set
* @param {*} model
*/
export function generateDashboard(model) {
var chosenObjects = getChosenObjects(model);
return chosenObjects.length ? chosenObjects.map((obj) => {
let object = _.find(objects, { value: obj });
return {
id: object.value,
name: object.name || object.value,
row: 0,
col: 0,
tile: {
backdrop_iconset: 'eclipse-smarthome-classic',
backdrop_icon: object.icon,
icon_size: 32
},
widgets: makeWidgets(object, model)
}
}) : '';
}
@@ -0,0 +1,32 @@
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue/dist/bootstrap-vue.esm'
import VueFormGenerator from 'vue-form-generator'
import Affix from 'vue-affix'
import Multiselect from 'vue-multiselect'
import VueResource from 'vue-resource'
import VueI18n from 'vue-i18n'
import 'daemonite-material/css/material.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import 'vue-form-generator/dist/vfg.css'
import 'vue-multiselect/dist/vue-multiselect.min.css'
import App from './App.vue'
import fieldMultiselect from './fieldMultiselect.vue'
Vue.use(BootstrapVue);
Vue.use(VueFormGenerator);
Vue.use(Affix);
Vue.use(VueResource);
Vue.use(VueI18n);
Vue.component('multiselect', Multiselect);
Vue.component('fieldMultiselect', fieldMultiselect);
let i18n = new VueI18n({
locale: 'en-UK'
});
const vm = new Vue({
i18n,
el: '#app',
render: h => h(App)
});
@@ -0,0 +1,213 @@
import * as _ from 'lodash'
import { floors, objects, OBJECTS_SUFFIX } from './definitions'
export const GROUP_PREFIX = 'g';
export function addFloors(floor, model) {
let items = [];
if (model.floorsCount > 1) {
items.push({
type: 'Group',
name: floor.abbr,
label: floor.name || floor.value,
category: model.itemsIcons ? floor.icon : '',
groupNames: ['Home'],
entryType: 'floor'
});
}
return items;
}
export function addRooms(floor, model) {
let items = [];
if (floor && floor.value && !_.isUndefined(model[floor.value])) {
model[floor.value].forEach((room) => {
let roomObjects = floor.value + '_' + room.value + OBJECTS_SUFFIX;
let floorPrefix = model.floorsCount > 1 ? floor.abbr + '_' : '';
items.push({
type: 'Group',
name: floorPrefix + room.value,
label: room.name || room.value,
category: model.itemsIcons ? room.icon : '',
groupNames: _.compact([
'Home',
model.floorsCount > 1 ? floor.abbr : ''
]),
entryType: 'room'
});
items = [
...items,
...addObjects(room, model, floorPrefix, roomObjects)
];
});
}
return items;
}
export function addObjects(room, model, floorPrefix, roomObjects) {
let objectCollection = model[roomObjects] || [];
if (!room.value && _.isEmpty(objectCollection)) {
return [];
}
return objectCollection.map(object => ({
type: _.first(object.type.split(':')),
name: floorPrefix + room.value + '_' + object.value,
label: object.name || object.value,
category: model.itemsIcons ? object.icon : '',
groupNames: [
floorPrefix + room.value,
GROUP_PREFIX + object.value
],
tags: addTags(object, model),
entryType: 'object'
}));
}
/**
* Generates a list of object groups
*
* @param {Object} model
* @return {string}
*/
export function addObjectGroups(model) {
let items = [];
let chosenObjects = getChosenObjects(model);
chosenObjects.forEach(function(dev) {
let object = _.find(objects, { value: dev });
if (object) {
let type = object.type.split(':');
let groupType = _.first(type);
let groupFuncName = type[1] ? type[1].split('(')[0] : '';
let groupFuncArgs = type[1] && type[1].split('(')[1] ? type[1].split('(')[1]
.split(',')
.map((arg) => arg.trim().replace(/\W/g, '')) : [];
let item = {
type: 'Group',
name: GROUP_PREFIX + object.value,
label: object.name || object.value,
category: model.itemsIcons ? object.icon : '',
groupNames: ['Home'],
groupType: groupType,
entryType: 'objectGroup'
};
if (groupFuncName) {
item = _.extend({}, item, {
function: {
name: groupFuncName
}
});
if (!_.isEmpty(groupFuncArgs)) {
item.function.params = groupFuncArgs;
}
}
items.push(item);
}
});
return _.uniq(items);
}
/**
* Gets list of objects chosen
* from collection
*
* @param {*} model
* @return {Array}
*/
export function getChosenObjects(model) {
return _.chain(model)
.pickBy((value, key) => _.endsWith(key, OBJECTS_SUFFIX))
.flatMap()
.map((item) => item.value)
.uniq()
.value() || [];
}
/**
* For a given object it creates a HomeKit-compatible
* set of tags.
*
* @param {Object} object
* @param {Object} model
* @return {Array}
*/
function addTags(object, model) {
var type = _.first(object.type.split(':'));
var tags = [];
switch (type) {
case 'Switch':
case 'Dimmer':
case 'Color':
tags.push('Switchable');
break;
default:
break;
}
switch (object.value) {
case 'Lamp':
case 'Light':
tags = ['Lighting'];
break;
case 'Motion':
tags = [];
break;
default:
break;
}
return model.itemsTags ? tags : [];
}
export function getItems(model) {
let items = [{
type: 'Group',
name: 'Home',
label: model.homeName,
category: model.itemsIcons ? 'house' : '',
entryType: 'home'
}];
for (var i = 0; i < model.floorsCount; i++) {
var floor = floors[i];
items = [
...items,
...addFloors(floor, model),
...addRooms(floor, model)
];
}
items = [
...items,
...addObjectGroups(model)
]
return items;
}
/**
* Returns array of Items
* without `entryType` which is not a valid parameter
* for the request
*
* @param {*} model
*/
export function generateItemsJson(model) {
let items = getItems(model);
return _.map(items, item => _.omit(item, ['entryType']));
}
@@ -0,0 +1,223 @@
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 60px;
--color-primary: #E64A19;
}
h1,
h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: var(--color-primary);
}
pre {
padding: 1em;
margin: 0;
max-height: 400px;
}
.vue-form-generator .form-group label {
width: 100%;
}
.field-checklist .wrapper {
width: 100%;
}
.vue-form-generator .form-control {
border-radius: 0;
border: 1px solid #e8e8e8;
box-shadow: none;
}
.vue-form-generator .field-checklist .listbox {
background: none;
border: 0;
box-shadow: none;
max-height: none;
padding-left: 0;
padding-right: 0;
}
@mixin checked-label() {
background: var(--color-primary);
color: #fff;
-webkit-box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.4);
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.4);
}
.list-row {
width: 33%;
display: inline-block;
padding: 10px;
input[type="checkbox"] {
display: none;
}
&.is-checked label {
@include checked-label();
}
}
.floors-number .radio-list label,
.list-row label {
width: 100%;
background-color: transparent;
background-image: none;
border: 0;
-webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
color: rgba(0, 0, 0, 0.87);
cursor: pointer;
display: inline-block;
font-size: 0.875rem;
font-weight: 500;
line-height: 1;
margin: 0;
max-width: 100%;
min-width: 5.5rem;
padding: 0.6875rem 1rem;
position: relative;
text-align: center;
text-transform: uppercase;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: middle;
white-space: nowrap;
border-radius: 2px;
-webkit-transition-duration: 0.3s;
-o-transition-duration: 0.3s;
transition-duration: 0.3s;
-webkit-transition-property: background-color, color, -webkit-box-shadow;
transition-property: background-color, color, -webkit-box-shadow;
-o-transition-property: background-color, box-shadow, color;
transition-property: background-color, box-shadow, color;
transition-property: background-color, box-shadow, color, -webkit-box-shadow;
-webkit-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
-o-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.field-switch input:checked~.label,
.multiselect__option--highlight,
.multiselect__tag {
background: var(--color-primary) !important;
}
.floors-number {
input[type="radio"] {
display: none;
}
.radio-list {
width: 100%;
}
.radio-list label {
border-radius: 0;
-webkit-box-shadow: none;
box-shadow: none;
display: inline-block !important;
width: 20% !important;
background: #ff9f5e;
color: #fff;
&.is-checked {
@include checked-label();
}
}
.radio-list label.is-checked ~ label {
background: transparent;
color: #000;
}
}
.multiselect__content-wrapper,
.multiselect__tags {
border-radius: 0;
}
.multiselect__option--highlight:after {
background: transparent;
}
.multiselect__tag-icon {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding-top: 2px;
&:after {
color: #fff;
font-size: 16px;
}
&:focus,
&:hover {
background: rgba(0, 0, 0, .15);
}
}
.card {
border-top-left-radius: 0;
border-top-right-radius: 0;
margin: 5px 0.25rem;
}
@mixin btn-muted() {
position: absolute;
top: .5rem;
z-index: 10;
display: block;
padding: .25rem .5rem;
font-size: 75%;
color: #fff;
cursor: pointer;
background-color: #ccc;
border: 0;
border-radius: .25rem;
&:hover {
background-color: #027de7;
}
}
.btn-clipboard {
@include btn-muted();
right: .5rem;
}
.btn-create-items {
@include btn-muted();
right: 5em;
}
@media only screen and (max-width: 599px) {
.navbar-toggler {
display: flex;
}
#preview {
width: 100%;
height: 100%;
background: #fff;
left: 0;
top: 56px !important;
padding-top: 3px;
position: fixed !important;
}
}
@@ -0,0 +1,120 @@
import * as _ from 'lodash'
import * as s from 'underscore.string'
import { floors } from './definitions'
import { getChosenObjects, getItems, GROUP_PREFIX } from './restItems'
export let sitemapName = '';
function getFloorItem(floor, model) {
let floorFrame = 'Frame ';
if (model.floorsCount > 1) {
let icon = model.itemsIcons && floor.icon ? '" icon="' + floor.icon : '';
floorFrame += 'label="' + (floor.name || floor.value) + icon + '" ';
}
return floorFrame + '{';
}
function getRoomGroups(floor, model, floorPrefix) {
if (floor && floor.value && !_.isUndefined(model[floor.value])) {
return model[floor.value].map(room =>
s.pad(' ', 8) + 'Group item=' + floorPrefix + room.value
);
}
return [];
}
function addFloorFrames(model) {
let lines = [];
for (let i = 0; i < model.floorsCount; i++) {
let floor = floors[i];
let floorPrefix = '';
if (model.floorsCount > 1) {
floorPrefix = floor.abbr + '_';
}
lines = [
...lines,
s.pad(' ', 4) + getFloorItem(floor, model),
...getRoomGroups(floor, model, floorPrefix),
s.pad(' ', 4) + '}'
];
if (i < model.floorsCount) {
lines.push('');
}
}
return lines;
}
function getTextGroup(group) {
let textGroup = `Text label="${group.label}"`;
textGroup += group.category && group.category !== 'none' ? ` icon="${group.category}" {` : ` {`;
return textGroup;
}
function getDefaultItems(groupItems, items) {
return groupItems.map(item => {
let room = _.find(items, { name: item.groupNames[0] });
return s.pad(' ', 12) + `Default item=${item.name} label="${room.label}"`;
});
}
function getObjectItems(model) {
const items = getItems(model);
let chosenObjects = getChosenObjects(model);
let objects = chosenObjects.map((object, index) => {
let groupName = GROUP_PREFIX + object;
let group = _.find(items, { name: groupName });
let groupItems = _.filter(items, item =>
item.groupNames && item.groupNames.includes(groupName)
);
let result = [
s.pad(' ', 8) + getTextGroup(group),
...getDefaultItems(groupItems, items),
s.pad(' ', 8) + '}'
];
if (index < chosenObjects.length - 1) {
result.push('');
}
return result;
});
return _.flatten(objects);
}
function addObjectsFrame(model) {
let objectItems = getObjectItems(model);
if (objectItems.length) {
return [
s.pad(' ', 4) + 'Frame {',
...objectItems,
s.pad(' ', 4) + '}'
];
}
return [];
}
export function generateSitemap(model) {
sitemapName = s(model.homeName)
.slugify()
.value()
.replace(/-/g, '_');
return [
'sitemap ' + sitemapName + ' label="' + model.homeName + '" {',
...addFloorFrames(model),
...addObjectsFrame(model),
'}'
].join('\n');
}
@@ -0,0 +1,167 @@
import * as _ from 'lodash'
import * as AsciiTable from 'ascii-table'
import { getItems } from './restItems'
/**
* Generates item's type
* e.g. `Switch` or `Group`
* or `Group:Switch:OR(ON, OFF)`
*
* @param {Object} item
* @param {Object} model
* @return {string}
*/
function generateType(item) {
let type = item.type;
if (item.entryType === 'objectGroup') {
type += ':' + item.groupType;
if (item.function) {
type += item.function.name ? ':' + item.function.name : '';
type += item.function.params ? '(' + item.function.params.join(', ') + ')' : '';
}
}
return type;
}
/**
* Generates a label for the Item
* @param {Object} item
* @return {string}
*/
function generateLabel(item) {
return '"' + (item.label || item.name) + '"';
}
/**
* Generates an icon if there's any.
*
* @param {Object} item
* @param {Object} model
* @return {string}
*/
function generateIcon(item, model) {
return item.category && model.itemsIcons ? '<' + item.category + '>' : null;
}
/**
* Generates a list of groups for the item.
* e.g. (Home, GF_Bedroom, gTemperature)
* @param {Object} item
* @return {string}
*/
function generateGroups(item) {
return _.isEmpty(item.groupNames) ? '' : '(' + item.groupNames.join(', ') + ')';
}
/**
* Generates a list of tags for the item.
* e.g. ["Switchable"]
* @param {Object} item
* @return {string}
*/
function generateTags(item) {
return !_.isEmpty(item.tags) ? '["' + item.tags.join('", "') + '"]' : '';
}
/**
* Generates a "channel" string for the object item
* e.g. `{channel=""}`
* @param {Object} item
* @param {Object} model
* @return {string}
*/
function generateChannel(item, model) {
return item.entryType === 'object' && model.itemsChannel ? '{channel=""}' : '';
}
/**
* Generates an array or items
* to be later processed by AsciiTable
*
* @param {*} items
*/
function generateTextualItems(items, model) {
let result = items.map(item => {
return [
generateType(item),
item.name,
generateLabel(item),
generateIcon(item, model),
generateGroups(item),
generateTags(item),
generateChannel(item, model)
]
});
// Add some spacing between blocks
if (!_.isEmpty(result)) {
result.push(['']);
}
return result;
}
/**
* Generates an array or items
* for a given type
*
* @param {string} entryType
* @param {Object} model
* @return {Array}
*/
function getItemsOfType(entryType, model) {
let allItems = getItems(model);
let items = _(allItems)
.filter({ entryType: entryType })
.uniq()
.value() || [];
return generateTextualItems(items, model);
}
/**
* Transforms array of lines
* into column-aligned table
* @param {Array} lines
*/
function toTable(lines) {
let table = new AsciiTable();
// Create the ascii-table to auto-format file
let result = table.addRowMatrix(lines)
.removeBorder()
.toString();
// Cleanup the lines
result = result.split('\n')
.map((line) => line.slice(2).trimRight())
.join('\n');
return result
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/**
* Generates a textual Items file based on user input
* Needs several datapoints from the model such as
* floorsCount, rooms collection, objects etc.
* @param {*} model
* @return {string}
*/
export function generateItems(model) {
// On the very top add the home item
let lines = [
...getItemsOfType('home', model),
...getItemsOfType('floor', model),
...getItemsOfType('room', model),
...getItemsOfType('object', model)
];
return toTable(lines) + '\n' +
toTable(getItemsOfType('objectGroup', model));
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

@@ -0,0 +1,83 @@
var path = require('path')
var webpack = require('webpack')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/dist/',
filename: 'build.js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
// Since sass-loader (weirdly) has SCSS as its default parse mode, we map
// the "scss" and "sass" values for the lang attribute to the right configs here.
// other preprocessors should work out of the box, no loader config like this necessary.
'scss': 'vue-style-loader!css-loader!sass-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
}
// other vue-loader options go here
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
},
{
test: /\.(scss|css)$/,
loader: 'style-loader!css-loader!sass-loader'
},
{
test: /\.(otf|eot|woff|woff2|ttf|svg)$/,
loader: 'file-loader'
}
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
devServer: {
historyApiFallback: true,
noInfo: true
},
performance: {
hints: false
},
devtool: '#eval-source-map'
}
if (process.env.NODE_ENV === 'production') {
module.exports.devtool = '#source-map'
// http://vue-loader.vuejs.org/en/workflow/production.html
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: false
}
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
])
}
+1
View File
@@ -27,6 +27,7 @@
<module>org.openhab.ui.dashboard</module> <module>org.openhab.ui.dashboard</module>
<module>org.openhab.ui.basicui</module> <module>org.openhab.ui.basicui</module>
<module>org.openhab.ui.classicui</module> <module>org.openhab.ui.classicui</module>
<module>org.openhab.ui.homebuilder</module>
<module>org.openhab.ui.paperui</module> <module>org.openhab.ui.paperui</module>
</modules> </modules>