Creating a Custom UI for openHAB 2

openHAB logo

Intro

OpenHAB is a home automation framework running on top of well known Java technologies like OSGi.

OpenHAB is developed in such a way that the average user could simply open up the prepackaged user interfaces and be able to start communicating with the devices in their home through available bindings. However, it’s often the case that openHAB is being used to develop an automation system for which configurations, settings, and other user interactions are being handled automatically, such as an internally developed application connecting devices in vehicles. In such a case, the default UIs are not appropriate for user interaction.

This article goes through the process of creating a custom UI which can be extended with all the desired behavior, and which does not include the undesirable behavior of the default UIs. Because openHAB is not well documented, this information can be very challenging to find anywhere else.

Creating the UI

Your UI file structure should be as follows. Everything should be under the org.eclipse.smarthome.ui.new package.

File Structure for openHAB UI
File Structure for openHAB UI

NewUIApp.java

NewUIApp.java has to take the httpService from OSGi and register its resources at a given location. This is done with the following code.

package org.eclipse.smarthome.ui.new.internal;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class NewUIApp {

    public static final String WEBAPP_ALIAS = "/newui"; // the root dir of our new ui
    private final Logger logger = LoggerFactory.getLogger(NewUIApp.class);

    protected HttpService httpService;

    protected void activate(ComponentContext componentContext) {
        try {
            // register our resources in the web directory
            httpService.registerResources(WEBAPP_ALIAS, "web", null);
            logger.info("Started New UI at " + WEBAPP_ALIAS);
        } catch (NamespaceException e) {
            logger.error("Error during servlet startup", e);
        }
    }

    protected void deactivate(ComponentContext componentContext) {
        httpService.unregister(WEBAPP_ALIAS);
        logger.info("Stopped New UI");
    }

    protected void setHttpService(HttpService httpService) {
        this.httpService = httpService;
    }

    protected void unsetHttpService(HttpService httpService) {
        this.httpService = null;
    }

}

MANIFEST.MF

The manifest file is used by OSGi to determine the name of the bundle and the bundle package requirements. That should look as follows.

Manifest-Version: 1.0
Bundle-Name: New UI
Bundle-Vendor: ExaminingEverything
Bundle-Version: 0.9.0.qualifier
Bundle-ManifestVersion: 2
Bundle-License: http://www.eclipse.org/legal/epl-v10.html
Import-Package: org.osgi.service.component,
 org.osgi.service.http,
 org.slf4j
Bundle-SymbolicName: org.eclipse.smarthome.ui.new;singleton:=true
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Service-Component: OSGI-INF/*.xml
Bundle-ClassPath: .

You’ll see how the bundle name comes into play later.

newuiapp.xml

This file tells OSGi what services our bundle provides. In this case, it simply provides the UI app Java class that we created, and it also includes a reference tag to get the HttpService injected at runtime.

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="activate" deactivate="deactivate" name="org.eclipse.smarthome.ui.new.internal.NewUIApp">
   <implementation class="org.eclipse.smarthome.ui.new.internal.NewUIApp"/>
   <reference bind="setHttpService" cardinality="1..1" interface="org.osgi.service.http.HttpService" name="HttpService" policy="static" unbind="unsetHttpService"/>
</scr:component>

Cardinality 1..1 refers to the fact that our request is satisfied by exactly one HttpService instance.

index.html

Index.html can contain whatever you like. This is there simply to show that the new UI is registered in openHAB. The next step for the UI, which this article does not cover, would be tying together the UI elements with API calls to the openHAB instance. You can look at the existing Paper UI implementation for an example of this.

Here’s a simple index.html page.

<!DOCTYPE HTML>
<html>
  <head>
    <title>New UI</title>
  </head>
  <body>
    <h1>NEW UI TEST!</h1>
  </body>
</html>

pom.xml

The pom.xml is used for building in Maven. This file will have to have all the necessary information to build the project as an eclipse plugin, and to create the correct output for an OSGi bundle so we can register it with openHAB later.

The POM should look like this. The reason for the giant POM is that all the parent POMs from the UI project POM all the way up to the smarthome library have to have their configurations included here for the build to work. Normally they would be included by their location relative to the UI POM, but we wont always want our POM located in a specific place.

<?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">

  <modelVersion>4.0.0</modelVersion>


  <artifactId>org.eclipse.smarthome.ui.new</artifactId>
  <groupId>org.eclipse.smarthome.ui</groupId>
  <version>0.9.0-SNAPSHOT</version>

  <name>New UI</name>
  <packaging>eclipse-plugin</packaging>

  <properties>
    <esh.java.version>1.8</esh.java.version>
    <maven.compiler.source>${esh.java.version}</maven.compiler.source>
    <maven.compiler.target>${esh.java.version}</maven.compiler.target>
    <maven.compiler.compilerVersion>${esh.java.version}</maven.compiler.compilerVersion>
    <tycho-version>1.0.0</tycho-version>
    <tycho-groupid>org.eclipse.tycho</tycho-groupid>
    <xtext-version>2.12.0</xtext-version>
    <karaf.version>4.0.3</karaf.version>
    <ds-annotations.version>1.2.8</ds-annotations.version>
    <jdt-annotations.version>2.1.0</jdt-annotations.version>
    <build.helper.maven.plugin.version>1.8</build.helper.maven.plugin.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

<build>
    <plugins>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>tycho-source-plugin</artifactId>
        <version>${tycho-version}</version>
        <executions>
          <execution>
            <id>plugin-source</id>
            <goals>
              <goal>plugin-source</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-scr-plugin</artifactId>
        <executions>
          <execution>
            <id>generate-scr-scrdescriptor</id>
            <goals>
              <goal>scr</goal>
            </goals>
          </execution>
        </executions>
      </plugin>   
<plugin>
        <groupId>${tycho-groupid}</groupId>
        <artifactId>tycho-maven-plugin</artifactId>
        <version>${tycho-version}</version>
        <extensions>true</extensions>
      </plugin>
      <plugin>
        <groupId>${tycho-groupid}</groupId>
        <artifactId>target-platform-configuration</artifactId>
        <configuration>
          <environments>
            <environment>
              <os>linux</os>
              <ws>gtk</ws>
              <arch>x86</arch>
            </environment>
            <environment>
              <os>linux</os>
              <ws>gtk</ws>
              <arch>x86_64</arch>
            </environment>
            <environment>
              <os>win32</os>
              <ws>win32</ws>
              <arch>x86</arch>
            </environment>
            <environment>
              <os>win32</os>
              <ws>win32</ws>
              <arch>x86_64</arch>
            </environment>
            <environment>
              <os>macosx</os>
              <ws>cocoa</ws>
              <arch>x86_64</arch>
            </environment>
          </environments>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-clean-plugin</artifactId>
        <version>2.5</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>${basedir}/xtend-gen</directory>
              <includes>
                <include>**</include>
              </includes>
              <excludes>
                <exclude>.gitignore</exclude>
              </excludes>
            </fileset>
            <fileset>
              <directory>${basedir}/src/main/generated-sources/xtend</directory>
              <includes>
                <include>**</include>
              </includes>
              <excludes>
                <exclude>.gitignore</exclude>
              </excludes>
            </fileset>
          </filesets>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
      </plugin>   
    </plugins>
<pluginManagement>
      <plugins>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>tycho-compiler-plugin</artifactId>
          <version>${tycho-version}</version>
          <configuration>
            <extraClasspathElements>
              <extraClasspathElement>
                <groupId>org.apache.felix</groupId>
                <artifactId>org.apache.felix.scr.ds-annotations</artifactId>
                <version>${ds-annotations.version}</version>
              </extraClasspathElement>
              <extraClasspathElement>
                <groupId>org.eclipse.jdt</groupId>
                <artifactId>org.eclipse.jdt.annotation</artifactId>
                <version>${jdt-annotations.version}</version>
              </extraClasspathElement>
            </extraClasspathElements>
            <compilerArgs>
              <arg>-err:+nullAnnot(org.eclipse.jdt.annotation.Nullable|org.eclipse.jdt.annotation.NonNull|org.eclipse.jdt.annotation.NonNullByDefault),+inheritNullAnnot</arg>
              <arg>-warn:+null,+inheritNullAnnot,+nullAnnotConflict,+nullUncheckedConversion,+nullAnnotRedundant,+nullDereference</arg>
            </compilerArgs>
          </configuration>          
        </plugin>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>target-platform-configuration</artifactId>
          <version>${tycho-version}</version>
          <configuration>
            <!--
            <resolver>p2</resolver>
            <ignoreTychoRepositories>true</ignoreTychoRepositories>
            -->
            <pomDependencies>consider</pomDependencies>
            <target>
              <artifact>
                <groupId>org.eclipse.smarthome</groupId>
                <artifactId>targetplatform</artifactId>
                <version>${project.version}</version>
                <classifier>smarthome</classifier>
              </artifact>
            </target>
          </configuration>
        </plugin>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>tycho-surefire-plugin</artifactId>
          <version>${tycho-version}</version>
          <configuration>
            <failIfNoTests>false</failIfNoTests>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.apache.felix</groupId>
          <artifactId>maven-scr-plugin</artifactId>
          <version>1.24.0</version>
          <configuration>
            <supportedProjectTypes>
              <supportedProjectType>eclipse-plugin</supportedProjectType>
            </supportedProjectTypes>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>build-helper-maven-plugin</artifactId>
          <version>${build.helper.maven.plugin.version}</version>
          <executions>
            <execution>
              <id>add-source</id>
              <phase>generate-sources</phase>
              <goals>
                <goal>add-source</goal>
              </goals>
              <configuration>
                <sources>
                  <source>src/main/groovy</source>
                </sources>
              </configuration>
            </execution>
            <execution>
              <id>add-test-source</id>
              <phase>generate-test-sources</phase>
              <goals>
                <goal>add-test-source</goal>
              </goals>
              <configuration>
                <sources>
                  <source>src/test/groovy</source>
                </sources>
              </configuration>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.apache.felix</groupId>
          <artifactId>maven-bundle-plugin</artifactId>
          <version>3.0.1</version>
          <extensions>true</extensions>
          <configuration>
            <supportedProjectTypes>
              <supportedProjectType>jar</supportedProjectType>
              <supportedProjectType>bundle</supportedProjectType>
              <supportedProjectType>eclipse-plugin</supportedProjectType>
            </supportedProjectTypes>
          </configuration>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.6.1</version>
          <configuration>
            <compilerId>groovy-eclipse-compiler</compilerId>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>compile</goal>
              </goals>
            </execution>
          </executions>
          <dependencies>
            <dependency>
              <groupId>org.codehaus.groovy</groupId>
              <artifactId>groovy-eclipse-compiler</artifactId>
              <version>2.9.2-01</version>
            </dependency>
            <dependency>
              <groupId>org.codehaus.groovy</groupId>
              <artifactId>groovy-eclipse-batch</artifactId>
              <version>2.4.3-01</version>
            </dependency>
          </dependencies>
        </plugin>
        <plugin>
          <groupId>com.mycila</groupId>
          <artifactId>license-maven-plugin</artifactId>
          <version>3.0</version>
          <configuration>
            <basedir>${basedir}</basedir>
            <header>src/etc/header.txt</header>
            <quiet>false</quiet>
            <failIfMissing>true</failIfMissing>
            <strictCheck>true</strictCheck>
            <aggregate>true</aggregate>
            <useDefaultMapping>true</useDefaultMapping>
            <mapping>
              <xtend>JAVADOC_STYLE</xtend>
              <mwe2>JAVADOC_STYLE</mwe2>
            </mapping>
            <includes>
              <include>src/**/*.java</include>
              <include>src/**/*.groovy</include>
              <include>src/**/*.xtend</include>
              <include>src/**/*.mwe2</include>
              <include>bin/**/*.mwe2</include>
              <include>workflows/**/*.mwe2</include>
              <include>src/main/feature/feature.xml</include>
              <include>feature.xml</include>
              <include>OSGI-INF/*.xml</include>
            </includes>
            <excludes>
              <exclude>_*.java</exclude>
            </excludes>
            <useDefaultExcludes>true</useDefaultExcludes>
            <properties>
              <year>2017</year>
            </properties>
            <encoding>UTF-8</encoding>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>check</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.eclipse.xtend</groupId>
          <artifactId>xtend-maven-plugin</artifactId>
          <version>${xtext-version}</version>
          <executions>
            <execution>
              <goals>
                <goal>compile</goal>
                <goal>xtend-install-debug-info</goal>
                <goal>testCompile</goal>
                <goal>xtend-test-install-debug-info</goal>
              </goals>
              <configuration>
                <outputDirectory>${basedir}/xtend-gen</outputDirectory>
                <testOutputDirectory>${basedir}/xtend-gen</testOutputDirectory>
              </configuration>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>tycho-versions-plugin</artifactId>
          <version>${tycho-version}</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-clean-plugin</artifactId>
          <version>2.5</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.scr.ds-annotations</artifactId>
      <version>${ds-annotations.version}</version>
      <optional>true</optional>
    </dependency>
  </dependencies> 

<profiles>
    <profile>
      <id>sign</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.eclipse.cbi.maven.plugins</groupId>
            <artifactId>eclipse-jarsigner-plugin</artifactId>
            <version>1.0.5</version>
            <executions>
              <execution>
                <id>sign</id>
                <phase>verify</phase>
                <goals>
                  <goal>sign</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
    <profile>
      <id>QA</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.7.4.201502262128</version>
            <configuration>
              <dataFile>${session.executionRootDirectory}/target/coverage.jacoco</dataFile>
              <destFile>${session.executionRootDirectory}/target/coverage.jacoco</destFile>
              <append>true</append>
              <excludes>
                <exclude>**/*Test.*</exclude>
              </excludes>
            </configuration>
            <executions>
              <execution>
                <id>default-prepare-agent</id>
                <goals>
                  <goal>prepare-agent</goal>
                </goals>
              </execution>
              <execution>
                <id>default-prepare-agent-integration</id>
                <goals>
                  <goal>prepare-agent-integration</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
    <!-- We need this profile in order to set '-Xdoclint:none' as a project property which will be used later by maven-javadoc-plugin as an 'additionalparam' to be passed to the javadoc.exe. -->
    <!-- This option will be used only if the JDK version is 1.8 or higher. Earlier versions of javadoc.exe does not accept this option. -->
    <profile>
      <id>doclint-java8-disable</id>
      <activation>
        <jdk>[1.8,)</jdk>
      </activation>
      <properties>
        <javadoc.opts>-Xdoclint:none</javadoc.opts>
      </properties>
    </profile>
    <profile>
      <id>javadoc</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>2.10.3</version>
            <executions>
              <execution>
                <id>aggregate</id>
                <goals>
                  <goal>aggregate-jar</goal>
                </goals>
              </execution>
            </executions>
            <configuration>
              <!-- 'javadoc.opts' project property is set by the 'doclint-java8-disable' profile. It is important to keep 'javadoc' profile declaration after the declaration of 'doclint-java8-disable' profile. -->
              <additionalparam>${javadoc.opts}</additionalparam>
              <excludePackageNames>*.internal.*,nl.*</excludePackageNames>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>build-helper-maven-plugin</artifactId>
            <version>${build.helper.maven.plugin.version}</version>
            <executions>
              <execution>
                <id>attach-artifacts</id>
                <phase>install</phase>
                <goals>
                  <goal>attach-artifact</goal>
                </goals>
                <configuration>
                  <artifacts>
                    <artifact>
                      <file>${project.build.outputDirectory}/${project.artifactId}-${project.version}.jar</file>
                      <type>jar</type>
                      <classifier>javadoc</classifier>
                    </artifact>
                  </artifacts>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles> 

</project>

build.properties

This file contains the build info for your jar output. Specifically, it tells Maven what items to include in the build and where to output the class files.


output.. = target/classes/
bin.includes = META-INF/,\
               .,\
               OSGI-INF/,\
               web/index.html
source.. = src/main/java/

Adding the bundle to OpenHAB

Package UI

First, you need to run the mvn package command in your base directory to create a package that you can register as a bundle with OSGi. On Windows, you may need to download Maven and extract it, then add it to your PATH. For Linux systems, you can run apt-get install maven. Your output should look like this:

openHAB UI Build Output
openHAB UI Build Output

Start openHAB

Installing openHAB is relatively easy on both Linux distros and Windows. Just unzip the files provided by openHAB, and run the start script from your shell. For Linux-based systems you may also have to ssh into the openHAB instance by running ssh openhab@localhost. The default password is habopen.

Install the bundle

The bundle:install command expects a URL, so you will have to type your package file path like so: file://localhost/[root_dir]/org.eclipse.smarthome.ui.new/target/org.eclipse.smarthome.ui.new-SNAPSHOT-[version].jar. Essentially, you provide the full path to the package jar as a file URL to register your new UI bundle.

Install OpenHAB Bundle
Install OpenHAB Bundle

Next, you should call bundle:start "New UI" to start the bundle.

Start openHAB UI Bundle
Start openHAB UI Bundle

View the page

Because we did not also register a dashboard tile bundle for the front-end, you must navigate directly to the new UI. You can do this by navigating to http://localhost:8080/newui/index.html in your browser. The HttpService that we had injected into our UI by OSGi will then serve up the index page from the location that we registered (web directory).

View openHAB UI
View openHAB UI

Updating the bundle

From now on, when you build your package, you only need to call bundle:update "New UI" and it will fetch it from the previous location. You will need to install the new bundle if the version changes, however.

Conclusion

OpenHAB is a great home automation system, but is designed more to be user-friendly rather than developer-friendly. There are many small tips and tricks like this needed to turn openHAB into a commercial production automation system. Creating a UI for openHAB is very simple – once you understand OSGi and the openHAB framework. This guide should provide you a starting point to develop your own system on top of openHAB.

Notes

Dashboard tiles

If you want your UI to show up in the list of tiles when the application opens, you’ll also have to extend the org.openhab.ui.dashboard.DashboardTile class, and provide your implementation of it to OSGi as a bundle. You can see how this is done by looking at the existing implementation for Paper UI.

API calls

The actual API calls needed to create an openHAB UI are varied and too many to go over here. The default openHAB installation provides a UI which allows you to test the various APIs, so you can use this as a starting point. You can also look at Paper UI if you want to see an implementation of the API calls based on AngularJS v1.