diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ac479062198d362bd8d66b53757cd6c7fc6fa7a4
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,24 @@
+image: homeassistant/amd64-builder
+
+variables:
+  REGISTRY: gcr.io/hedenstroem-docker
+
+before_script:
+  - cat $GCR_CREDENTIALS | docker login -u _json_key --password-stdin https://gcr.io
+
+stages:
+  - build
+
+.build: &build
+  stage: build
+  only:
+    - master
+    - /^\d+[.]\d+[.]\d+$/
+  script:
+    - /usr/bin/builder.sh -t $CI_PROJECT_DIR/${CI_JOB_NAME%%_*} --${CI_JOB_NAME#*_} -i ${CI_JOB_NAME%%_*}-addon-{arch} -d $REGISTRY
+
+"nipca-motion_aarch64": *build
+"nipca-motion_amd64": *build
+"nipca-motion_armv7": *build
+"nipca-motion_armhf": *build
+"nipca-motion_i386": *build
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000000000000000000000000000000000000..d4692f10988d077cf8e462a308d696b2f23f84ac
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,29 @@
+{
+  // See https://go.microsoft.com/fwlink/?LinkId=733558
+  // for the documentation about the tasks.json format
+  "version": "2.0.0",
+  "tasks": [
+    {
+      "label": "Start Home Assistant",
+      "type": "shell",
+      "command": "sudo ./.devcontainer/supervisor.sh",
+      "group":"test",
+      "presentation": {
+        "reveal": "always",
+        "panel": "new"
+      },
+      "problemMatcher": []
+    },
+    {
+      "label": "Run Home Assistant CLI",
+      "type": "shell",
+      "command": "docker exec -ti hassio_cli /usr/bin/cli.sh",
+      "group": "test",
+      "presentation": {
+        "reveal": "always",
+        "panel": "new"
+      },
+      "problemMatcher": []
+    }
+  ]
+}
diff --git a/nipca-motion/CHANGELOG.md b/nipca-motion/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..6629d09c78195037481f47d2eb05d26000802645
--- /dev/null
+++ b/nipca-motion/CHANGELOG.md
@@ -0,0 +1,3 @@
+## [0.1.0 2021-4-26]
+### Fixes
+- TBD
diff --git a/nipca-motion/config.json b/nipca-motion/config.json
index 62e77f46ceac4ea9e57a118703fb1d7ff10db19e..ddadb50bdb85e93a3510c3eecbd17866b7e58010 100644
--- a/nipca-motion/config.json
+++ b/nipca-motion/config.json
@@ -1,23 +1,28 @@
 {
   "name": "NIPCA Motion",
-  "version": "0.3",
+  "version": "0.1",
   "slug": "nipca-motion",
   "description": "Motion and sound sensors for NIPCA-compatible cameras",
-  "url": "https://gitlab.hedenstroem.com/hassio/addons/nipca-motion",
+  "url": "https://gitlab.hedenstroem.com/home-assistant/addons/-/tree/master/nipca-motion",
   "init": false,
   "services": ["mqtt:need"],
   "hassio_api": true,
   "arch": ["aarch64", "amd64", "armhf", "armv7", "i386"],
   "options": {
     "log_level": "info",
+    "retry_delay": 60,
+    "keep_alive_timeout": 30,
     "cameras": [
       { "url": "http://localhost", "username": "admin", "password": "password" }
     ]
   },
   "schema": {
     "log_level": "list(trace|debug|info|warn|error|fatal|panic)",
+    "retry_delay": "int(5,300)",
+    "keep_alive_timeout": "int(5,300)",
     "cameras": [
       { "url": "str", "username": "str", "password": "str" }
     ]
-  }
+  },
+  "image": "gcr.io/hedenstroem-docker/nipca-motion-addon-{arch}"
 }
diff --git a/nipca-motion/src/camera.go b/nipca-motion/src/camera.go
new file mode 100644
index 0000000000000000000000000000000000000000..338deca27138eb303a72a03ccdcac90925929fa2
--- /dev/null
+++ b/nipca-motion/src/camera.go
@@ -0,0 +1,122 @@
+package main
+
+import (
+	"bufio"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/spf13/viper"
+)
+
+type Camera struct {
+	Name     string
+	URL      string
+	Username string
+	Password string
+}
+
+type StreamResponse struct {
+	camera Camera
+	fields []string
+	err    error
+}
+
+func (c *Camera) getConfig() (*viper.Viper, error) {
+
+	client := &http.Client{
+		Timeout: time.Second * 10,
+	}
+
+	req, err := http.NewRequest("GET", c.URL+"/common/info.cgi", nil)
+	if err != nil {
+		return nil, err
+	}
+	req.SetBasicAuth(c.Username, c.Password)
+
+	res, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	defer res.Body.Close()
+	bytes, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	data := make(map[string]interface{})
+	for _, line := range strings.Split(string(bytes), "\n") {
+		fields := strings.Split(strings.TrimSpace(string(line)), "=")
+		if len(fields) > 1 {
+			data[fields[0]] = fields[1]
+		}
+	}
+
+	log.Debugf("%s info: %+v", c.URL, data)
+
+	config := viper.New()
+	config.MergeConfigMap(data)
+
+	c.Name = config.GetString("name")
+
+	return config, nil
+}
+
+func (c Camera) openStream(q chan StreamResponse) {
+
+	client := &http.Client{
+		Timeout: 0,
+	}
+
+	req, err := http.NewRequest("GET", c.URL+"/config/notify_stream.cgi", nil)
+	if err != nil {
+		q <- StreamResponse{c, nil, err}
+		return
+	}
+	req.SetBasicAuth(c.Username, c.Password)
+
+	res, err := client.Do(req)
+	if err != nil {
+		q <- StreamResponse{c, nil, err}
+		return
+	}
+
+	log.Infof("%s stream open", c.Name)
+
+	defer res.Body.Close()
+	reader := bufio.NewReader(res.Body)
+
+	cf := make(chan []string, 1)
+	ce := make(chan error, 1)
+
+	go func() {
+		for {
+			s, e := reader.ReadString('\n')
+			if e != nil {
+				ce <- e
+				return
+			}
+			cf <- strings.Split(strings.TrimSpace(s), "=")
+		}
+	}()
+
+	delay := viper.GetDuration("keep_alive_timeout") * time.Second
+	for {
+		select {
+		case f := <-cf:
+			if len(f) > 0 && f[0] != "" {
+				q <- StreamResponse{c, f, nil}
+			}
+		case e := <-ce:
+			q <- StreamResponse{c, nil, e}
+			return
+		case <-time.After(delay):
+			q <- StreamResponse{c, nil, errors.New("keep alive timeout")}
+			return
+		}
+	}
+
+}
diff --git a/nipca-motion/src/go.mod b/nipca-motion/src/go.mod
index f79158f4e708769fd584ad4e6e66e8fe68f85dcb..37dd3e0d07c8a21fb26b7b2d054faa4c8878b815 100644
--- a/nipca-motion/src/go.mod
+++ b/nipca-motion/src/go.mod
@@ -3,7 +3,6 @@ module gitlab.hedenstroem.com/hassio/addons/nipca-motion/nipca-motion
 go 1.16
 
 require (
-	github.com/davecgh/go-spew v1.1.1
 	github.com/eclipse/paho.mqtt.golang v1.3.3
 	github.com/joho/godotenv v1.3.0
 	github.com/sirupsen/logrus v1.8.1
diff --git a/nipca-motion/src/hass.go b/nipca-motion/src/hass.go
new file mode 100644
index 0000000000000000000000000000000000000000..b061b6cef225f92f26d8a0b4e94431fb060c829b
--- /dev/null
+++ b/nipca-motion/src/hass.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+	"fmt"
+)
+
+type Device struct {
+	Identifiers     string `json:"identifiers" mapstructure:"macaddr"`
+	Manufacturer    string `json:"manufacturer" mapstructure:"brand"`
+	Model           string `json:"model"`
+	Name            string `json:"name"`
+	SoftwareVersion string `json:"sw_version" mapstructure:"version"`
+}
+
+type Availability struct {
+	Topic string `json:"topic"`
+}
+
+type Config struct {
+	Availability     []Availability `json:"availability"`
+	AvailabilityMode string         `json:"availability_mode"`
+	UniqueId         string         `json:"unique_id"`
+	Device           Device         `json:"device"`
+	DeviceClass      string         `json:"device_class"`
+	Name             string         `json:"name"`
+	StateTopic       string         `json:"state_topic"`
+}
+
+func publishDiscoveryInformation(mqttClient MqttClient, camera *Camera) error {
+
+	cameraConfig, err := camera.getConfig()
+	if err != nil {
+		return err
+	}
+
+	device := Device{}
+	cameraConfig.Unmarshal(&device)
+
+	prefix := fmt.Sprintf("%s/binary_sensor/%s", mqttClient.Config.DiscoveryPrefix, device.Name)
+
+	mqttClient.Publish(prefix+"/availability", 1, true, "offline")
+	if err != nil {
+		return err
+	}
+
+	config := Config{
+		Availability: []Availability{
+			{mqttClient.Config.DiscoveryPrefix + "/addon/nipca-motion/availability"},
+			{prefix + "/availability"},
+		},
+		AvailabilityMode: "all",
+		Device:           device,
+	}
+
+	config.UniqueId = device.Name + "-MOTION"
+	config.DeviceClass = "motion"
+	config.Name = device.Name + " Motion"
+	config.StateTopic = prefix + "/Motion/state"
+
+	log.Debugf("%s: %+v", config.Name, config)
+
+	err = mqttClient.Publish(prefix+"/Motion/config", 1, true, config)
+	if err != nil {
+		return err
+	}
+
+	config.UniqueId = device.Name + "-SOUND"
+	config.DeviceClass = "sound"
+	config.Name = device.Name + " Sound"
+	config.StateTopic = prefix + "/Sound/state"
+
+	log.Debugf("%s: %+v", config.Name, config)
+
+	return mqttClient.Publish(prefix+"/Sound/config", 1, true, config)
+
+}
diff --git a/nipca-motion/src/main.go b/nipca-motion/src/main.go
index fdb2867451d29f1ae5c83048fdd09e63cd452eaf..84bd530bc96232b03e799a15f8818cf8b169e243 100644
--- a/nipca-motion/src/main.go
+++ b/nipca-motion/src/main.go
@@ -1,11 +1,8 @@
 package main
 
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
-	"io/ioutil"
-	"net/http"
+	"strings"
 	"time"
 
 	mqtt "github.com/eclipse/paho.mqtt.golang"
@@ -14,38 +11,8 @@ import (
 	"github.com/spf13/viper"
 )
 
-type MqttConfiguration struct {
-	Host            string `json:"host"`
-	Port            int    `json:"port"`
-	SSL             bool   `json:"ssl"`
-	Protocol        string `json:"protocol"`
-	Username        string `json:"username"`
-	Password        string `json:"password"`
-	Addon           string `json:"addon"`
-	ClientID        string `mapstructure:"client_id"`
-	DiscoveryPrefix string `mapstructure:"discovery_prefix"`
-}
-
-type MqttService struct {
-	Result  string            `json:"result"`
-	Message string            `json:"message"`
-	Config  MqttConfiguration `json:"data"`
-}
-
 var log *logrus.Logger
 
-type MqttLogger struct {
-	level logrus.Level
-}
-
-func (logger MqttLogger) Println(args ...interface{}) {
-	log.Log(logger.level, args)
-}
-
-func (logger MqttLogger) Printf(format string, args ...interface{}) {
-	log.Logf(logger.level, format, args)
-}
-
 func loadOptions() {
 	godotenv.Load()
 	viper.AutomaticEnv()
@@ -58,6 +25,8 @@ func loadOptions() {
 	viper.BindEnv("discovery_prefix", "MQTT_DISCOVERY_PREFIX")
 	viper.RegisterAlias("token", "SUPERVISOR_TOKEN")
 	viper.SetDefault("log_level", "info")
+	viper.SetDefault("retry_delay", 60)
+	viper.SetDefault("keep_alive_timeout", 30)
 	viper.SetDefault("discovery_prefix", "homeassistant")
 	viper.SetDefault("config_path", "/data")
 	viper.SetDefault("config_name", "options")
@@ -67,79 +36,11 @@ func loadOptions() {
 	if err := viper.ReadInConfig(); err != nil {
 		log.Warn(err)
 	}
-	if viper.GetString("log_level") == "debug" {
+	if viper.GetString("log_level") == "trace" {
 		viper.Debug()
 	}
 }
 
-func getMqttConfiguration() (MqttConfiguration, error) {
-
-	service := MqttService{}
-	viper.Unmarshal(&service.Config)
-
-	token := viper.GetString("token")
-	if token == "" {
-		return service.Config, nil
-	}
-
-	client := &http.Client{
-		Timeout: time.Second * 10,
-	}
-
-	req, err := http.NewRequest("GET", "http://supervisor/services/mqtt", nil)
-	if err != nil {
-		return service.Config, err
-	}
-	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
-
-	res, err := client.Do(req)
-	if err != nil {
-		return service.Config, err
-	}
-
-	defer res.Body.Close()
-	bytes, err := ioutil.ReadAll(res.Body)
-	if err != nil {
-		return service.Config, err
-	}
-
-	err = json.Unmarshal(bytes, &service)
-
-	if service.Result == "error" {
-		return service.Config, errors.New("MQTT Configuration: " + service.Message)
-	}
-
-	return service.Config, err
-
-}
-
-func openMqttConnection(config MqttConfiguration) (mqtt.Client, error) {
-	scheme := "tcp"
-	if config.SSL {
-		scheme = "ssl"
-	}
-	broker := fmt.Sprintf("%s://%s:%d", scheme, config.Host, config.Port)
-	availabilityTopic := fmt.Sprintf("%s/addon/nipca-motion/availability", config.DiscoveryPrefix)
-	opts := mqtt.NewClientOptions()
-	opts.AddBroker(broker).SetClientID(config.ClientID)
-	opts.SetWill(availabilityTopic, "offline", 1, true)
-	opts.SetUsername(config.Username).SetPassword(config.Password)
-	client := mqtt.NewClient(opts)
-	token := client.Connect()
-	token.Wait()
-	err := token.Error()
-	if err != nil {
-		return nil, err
-	}
-	token = client.Publish(availabilityTopic, 1, true, "online")
-	token.Wait()
-	err = token.Error()
-	if err != nil {
-		return nil, err
-	}
-	return client, nil
-}
-
 func main() {
 
 	log = logrus.New()
@@ -148,11 +49,11 @@ func main() {
 	if err == nil {
 		log.SetLevel(level)
 	}
-	log.Info("Log level: ", level)
+	log.Infof("Log level: %s", level)
 	mqtt.ERROR = MqttLogger{level: logrus.ErrorLevel}
 	mqtt.CRITICAL = MqttLogger{level: logrus.ErrorLevel}
 	mqtt.WARN = MqttLogger{level: logrus.WarnLevel}
-	mqtt.DEBUG = MqttLogger{level: logrus.DebugLevel}
+	mqtt.DEBUG = MqttLogger{level: logrus.TraceLevel}
 
 	mqttConfig, err := getMqttConfiguration()
 	if err != nil {
@@ -164,6 +65,57 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
-	mqttClient.IsConnected()
+
+	var cameras []Camera
+	viper.UnmarshalKey("cameras", &cameras)
+	log.Debugf("Cameras: %+v", cameras)
+
+	q := make(chan StreamResponse)
+
+	for _, camera := range cameras {
+		err := publishDiscoveryInformation(mqttClient, &camera)
+		if err != nil {
+			log.Warnf("%s: %s", camera.URL, err)
+		}
+		go camera.openStream(q)
+	}
+
+	delay := viper.GetDuration("retry_delay") * time.Second
+	for {
+		res := <-q
+		if res.err == nil {
+			log.Debugf("%s: %+v", res.camera.Name, res.fields)
+			switch res.fields[0] {
+			case "md1":
+				topic := fmt.Sprintf("%s/binary_sensor/%s/Motion/state", mqttClient.Config.DiscoveryPrefix, res.camera.Name)
+				mqttClient.Publish(topic, 0, true, strings.ToUpper(res.fields[1]))
+			case "audio_detected":
+				topic := fmt.Sprintf("%s/binary_sensor/%s/Sound/state", mqttClient.Config.DiscoveryPrefix, res.camera.Name)
+				mqttClient.Publish(topic, 0, true, strings.ToUpper(res.fields[1]))
+			case "cameraname":
+				topic := fmt.Sprintf("%s/binary_sensor/%s/availability", mqttClient.Config.DiscoveryPrefix, res.camera.Name)
+				mqttClient.Publish(topic, 1, true, "online")
+			case "keep_alive":
+				topic := fmt.Sprintf("%s/binary_sensor/%s/availability", mqttClient.Config.DiscoveryPrefix, res.camera.Name)
+				mqttClient.Publish(topic, 0, true, "online")
+			}
+		} else {
+			if res.camera.Name != "" {
+				log.Warnf("%s: %s, retrying in %s", res.camera.Name, res.err, delay)
+				topic := fmt.Sprintf("%s/binary_sensor/%s/availability", mqttClient.Config.DiscoveryPrefix, res.camera.Name)
+				mqttClient.Publish(topic, 1, true, "offline")
+			} else {
+				log.Warnf("%s: %s, retrying in %s", res.camera.URL, res.err, delay)
+			}
+			go func() {
+				time.Sleep(delay)
+				err := publishDiscoveryInformation(mqttClient, &res.camera)
+				if err != nil {
+					log.Warnf("%s: %s", res.camera.URL, err)
+				}
+				res.camera.openStream(q)
+			}()
+		}
+	}
 
 }
diff --git a/nipca-motion/src/mqtt.go b/nipca-motion/src/mqtt.go
new file mode 100644
index 0000000000000000000000000000000000000000..9ab429c053d304220176372707a26826cf538157
--- /dev/null
+++ b/nipca-motion/src/mqtt.go
@@ -0,0 +1,135 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"reflect"
+	"time"
+
+	mqtt "github.com/eclipse/paho.mqtt.golang"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/viper"
+)
+
+type MqttConfig struct {
+	Host            string `json:"host"`
+	Port            int    `json:"port"`
+	SSL             bool   `json:"ssl"`
+	Protocol        string `json:"protocol"`
+	Username        string `json:"username"`
+	Password        string `json:"password"`
+	Addon           string `json:"addon"`
+	ClientID        string `mapstructure:"client_id"`
+	DiscoveryPrefix string `mapstructure:"discovery_prefix"`
+}
+
+type MqttService struct {
+	Result  string     `json:"result"`
+	Message string     `json:"message"`
+	Config  MqttConfig `json:"data"`
+}
+
+type MqttLogger struct {
+	level logrus.Level
+}
+
+func (logger MqttLogger) Println(args ...interface{}) {
+	log.Log(logger.level, args)
+}
+
+func (logger MqttLogger) Printf(format string, args ...interface{}) {
+	log.Logf(logger.level, format, args)
+}
+
+type MqttClient struct {
+	Config MqttConfig
+	client mqtt.Client
+}
+
+func (mc MqttClient) Publish(topic string, qos byte, retained bool, payload interface{}) (err error) {
+	msg := payload
+	if reflect.TypeOf(payload).String() != "string" {
+		msg, err = json.MarshalIndent(payload, "", "  ")
+		if err != nil {
+			return
+		}
+	}
+	token := mc.client.Publish(topic, qos, retained, msg)
+	token.Wait()
+	return token.Error()
+}
+
+func getMqttConfiguration() (MqttConfig, error) {
+
+	service := MqttService{}
+	viper.Unmarshal(&service.Config)
+
+	token := viper.GetString("token")
+	if token == "" {
+		return service.Config, nil
+	}
+
+	client := &http.Client{
+		Timeout: time.Second * 10,
+	}
+
+	req, err := http.NewRequest("GET", "http://supervisor/services/mqtt", nil)
+	if err != nil {
+		return service.Config, err
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
+
+	res, err := client.Do(req)
+	if err != nil {
+		return service.Config, err
+	}
+
+	defer res.Body.Close()
+	bytes, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return service.Config, err
+	}
+
+	err = json.Unmarshal(bytes, &service)
+
+	if service.Result == "error" {
+		return service.Config, errors.New("MQTT Configuration: " + service.Message)
+	}
+
+	return service.Config, err
+
+}
+
+func openMqttConnection(config MqttConfig) (MqttClient, error) {
+
+	scheme := "tcp"
+	if config.SSL {
+		scheme = "ssl"
+	}
+	broker := fmt.Sprintf("%s://%s:%d", scheme, config.Host, config.Port)
+	availabilityTopic := config.DiscoveryPrefix + "/addon/nipca-motion/availability"
+	opts := mqtt.NewClientOptions()
+	opts.AddBroker(broker).SetClientID(config.ClientID)
+	opts.SetWill(availabilityTopic, "offline", 1, true)
+	opts.SetUsername(config.Username).SetPassword(config.Password)
+
+	client := mqtt.NewClient(opts)
+	token := client.Connect()
+	token.Wait()
+	err := token.Error()
+	if err != nil {
+		return MqttClient{}, err
+	}
+
+	c := MqttClient{config, client}
+	err = c.Publish(availabilityTopic, 1, true, "online")
+	if err != nil {
+		return c, err
+	}
+
+	log.Infof("MQTT Client connected to %s", broker)
+	return c, nil
+}