Skip to content

Commit 73e2e76

Browse files
committed
Add Hue service
1 parent 1baaa6e commit 73e2e76

File tree

19 files changed

+644
-0
lines changed

19 files changed

+644
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
version: '3'
2+
3+
services:
4+
service.config:
5+
build: ./service.config
6+
volumes:
7+
- ./service.config:/go/src/github.com/jakewright/tutorials/home-automation/04-hue-lights/service.config
8+
- ./service.config/config.yaml:/data/config.yaml
9+
ports:
10+
- 7000:80
11+
12+
service.registry.device:
13+
build: ./service.registry.device
14+
volumes:
15+
- ./service.registry.device:/usr/src/app
16+
ports:
17+
- 7001:80
18+
19+
service.controller.hue:
20+
build: ./service.controller.hue
21+
volumes:
22+
- ./service.controller.hue:/usr/src/app
23+
- /usr/src/app/node_modules
24+
ports:
25+
- 7003:80
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM golang:latest
2+
RUN go get -u golang.org/x/lint/golint
3+
RUN go get github.com/githubnemo/CompileDaemon
4+
5+
WORKDIR /go/src/github.com/jakewright/tutorials/home-automation/04-hue-lights/service.config
6+
COPY . .
7+
8+
RUN go get -d -v ./...
9+
RUN go install -v ./...
10+
11+
CMD CompileDaemon -build="go install ." -command="/go/bin/service.config"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
service.controller.hue:
2+
hueBridge:
3+
host: http://192.168.1.110
4+
username: g3avkmfKYFZtxwb6Ny7DNQgQXJtn7Sl3VzEcbJDi
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package controller
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/gorilla/mux"
9+
"github.com/jakewright/tutorials/home-automation/04-hue-lights/service.config/domain"
10+
)
11+
12+
// Controller exports the handlers for the endpoints
13+
type Controller struct {
14+
Config *domain.Config
15+
}
16+
17+
// ReadConfig writes the config for the given service to the ResponseWriter
18+
func (c *Controller) ReadConfig(w http.ResponseWriter, r *http.Request) {
19+
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
20+
21+
vars := mux.Vars(r)
22+
serviceName, ok := vars["serviceName"]
23+
if !ok {
24+
w.WriteHeader(http.StatusBadRequest)
25+
fmt.Fprintf(w, "serviceName not set")
26+
return
27+
}
28+
29+
config, err := c.Config.Get(serviceName)
30+
if err != nil {
31+
w.WriteHeader(http.StatusInternalServerError)
32+
fmt.Fprintf(w, err.Error())
33+
return
34+
}
35+
36+
rsp, err := json.Marshal(&config)
37+
if err != nil {
38+
w.WriteHeader(http.StatusInternalServerError)
39+
fmt.Fprintf(w, err.Error())
40+
return
41+
}
42+
43+
w.WriteHeader(http.StatusOK)
44+
fmt.Fprintf(w, string(rsp))
45+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package domain
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
"gopkg.in/yaml.v2"
8+
)
9+
10+
// Config is an abstraction around the map that holds the config values
11+
type Config struct {
12+
config map[string]interface{}
13+
lock sync.RWMutex
14+
}
15+
16+
// SetFromBytes sets the internal config based on a byte array of YAML
17+
func (c *Config) SetFromBytes(data []byte) error {
18+
var rawConfig interface{}
19+
if err := yaml.Unmarshal(data, &rawConfig); err != nil {
20+
return err
21+
}
22+
23+
untypedConfig, ok := rawConfig.(map[interface{}]interface{})
24+
if !ok {
25+
return fmt.Errorf("config is not a map")
26+
}
27+
28+
config, err := convertKeysToStrings(untypedConfig)
29+
if err != nil {
30+
return err
31+
}
32+
33+
c.lock.Lock()
34+
defer c.lock.Unlock()
35+
36+
c.config = config
37+
return nil
38+
}
39+
40+
// Get returns the config for a particular service
41+
func (c *Config) Get(serviceName string) (map[string]interface{}, error) {
42+
c.lock.RLock()
43+
defer c.lock.RUnlock()
44+
45+
var a map[string]interface{}
46+
if _, ok := c.config["base"]; ok {
47+
a, ok = c.config["base"].(map[string]interface{})
48+
if !ok {
49+
return nil, fmt.Errorf("base config is not a map")
50+
}
51+
}
52+
53+
// If no config is defined for the service
54+
if _, ok := c.config[serviceName]; !ok {
55+
// Return the base config
56+
return a, nil
57+
}
58+
59+
b, ok := c.config[serviceName].(map[string]interface{})
60+
if !ok {
61+
return nil, fmt.Errorf("service %q config is not a map", serviceName)
62+
}
63+
64+
// Merge the maps with the service config taking precedence
65+
config := make(map[string]interface{})
66+
for k, v := range a {
67+
config[k] = v
68+
}
69+
for k, v := range b {
70+
config[k] = v
71+
}
72+
73+
return config, nil
74+
}
75+
76+
func convertKeysToStrings(m map[interface{}]interface{}) (map[string]interface{}, error) {
77+
n := make(map[string]interface{})
78+
79+
for k, v := range m {
80+
str, ok := k.(string)
81+
if !ok {
82+
return nil, fmt.Errorf("config key is not a string")
83+
}
84+
85+
if vMap, ok := v.(map[interface{}]interface{}); ok {
86+
var err error
87+
v, err = convertKeysToStrings(vMap)
88+
if err != nil {
89+
return nil, err
90+
}
91+
}
92+
93+
n[str] = v
94+
}
95+
96+
return n, nil
97+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"time"
6+
7+
"github.com/jakewright/muxinator"
8+
9+
"github.com/jakewright/tutorials/home-automation/04-hue-lights/service.config/controller"
10+
"github.com/jakewright/tutorials/home-automation/04-hue-lights/service.config/domain"
11+
"github.com/jakewright/tutorials/home-automation/04-hue-lights/service.config/service"
12+
)
13+
14+
func main() {
15+
config := domain.Config{}
16+
17+
configService := service.ConfigService{
18+
Config: &config,
19+
Location: "/data/config.yaml",
20+
}
21+
22+
go configService.Watch(time.Second * 30)
23+
24+
c := controller.Controller{
25+
Config: &config,
26+
}
27+
28+
router := muxinator.NewRouter()
29+
router.Get("/read/{serviceName}", c.ReadConfig)
30+
log.Fatal(router.ListenAndServe(":80"))
31+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package service
2+
3+
import (
4+
"io/ioutil"
5+
"log"
6+
"time"
7+
8+
"github.com/jakewright/tutorials/home-automation/04-hue-lights/service.config/domain"
9+
)
10+
11+
type ConfigService struct {
12+
Config *domain.Config
13+
Location string
14+
}
15+
16+
// Watch reloads the config every d duration
17+
func (s *ConfigService) Watch(d time.Duration) {
18+
for {
19+
err := s.Reload()
20+
if err != nil {
21+
log.Print(err)
22+
}
23+
24+
time.Sleep(d)
25+
}
26+
}
27+
28+
// Reload reads the config and applies changes
29+
func (s *ConfigService) Reload() error {
30+
data, err := ioutil.ReadFile(s.Location)
31+
if err != nil {
32+
return err
33+
}
34+
35+
err = s.Config.SetFromBytes(data)
36+
if err != nil {
37+
return err
38+
}
39+
40+
return nil
41+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM node:8.11
2+
3+
# Install nodemon
4+
RUN npm install -g nodemon
5+
6+
# Create app directory
7+
RUN mkdir -p /usr/src/app
8+
WORKDIR /usr/src/app
9+
10+
# Install app dependencies
11+
COPY package.json .
12+
RUN npm install
13+
14+
CMD [ "npm", "start" ]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const axios = require("axios");
2+
3+
class HueClient {
4+
get hueUrl() {
5+
return `${this.host}/api/${this.username}`;
6+
}
7+
8+
async fetchAllState() {
9+
const rsp = await axios.get(`${this.hueUrl}/lights`);
10+
11+
const lights = {};
12+
13+
for (const hueId in rsp.data) {
14+
lights[hueId] = {
15+
power: rsp.data[hueId].state.on,
16+
brightness: rsp.data[hueId].state.bri
17+
};
18+
}
19+
20+
return lights;
21+
}
22+
23+
async fetchState(hueId) {
24+
const rsp = await axios.get(`${this.hueUrl}/lights/${hueId}`);
25+
26+
return {
27+
power: rsp.data.state.on,
28+
brightness: rsp.data.state.bri
29+
};
30+
}
31+
32+
async applyState(hueId, state) {
33+
const hueState = {
34+
on: state.power,
35+
bri: state.brightness,
36+
};
37+
38+
await axios.put(`${this.hueUrl}/lights/${hueId}/state`, hueState);
39+
return this.fetchState(hueId);
40+
}
41+
}
42+
43+
const hueClient = new HueClient();
44+
exports = module.exports = hueClient;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const axios = require("axios");
2+
const hueClient = require("../api/hueClient");
3+
4+
let devices = [];
5+
6+
const findByHueId = (hueId) => {
7+
return devices.find(device => device.attributes["hue_id"] === hueId);
8+
};
9+
10+
const findByIdentifier = (identifier) => {
11+
return devices.find(device => device.identifier === identifier);
12+
};
13+
14+
const fetchAllState = async () => {
15+
const rsp = await axios.get("http://service.registry.device/devices");
16+
devices = rsp.data.data;
17+
18+
const hueIdToState = await hueClient.fetchAllState();
19+
20+
for (const hueId in hueIdToState) {
21+
const device = findByHueId(hueId);
22+
if (device === undefined) continue;
23+
24+
Object.assign(device, hueIdToState[hueId]);
25+
}
26+
};
27+
28+
const applyState = async (device, state) => {
29+
const newState = await hueClient.applyState(device.attributes["hue_id"], state);
30+
Object.assign(device, newState);
31+
};
32+
33+
exports = module.exports = { findByIdentifier, fetchAllState, applyState };
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const axios = require("axios");
2+
const express = require("express");
3+
const dao = require("./dao");
4+
const hueClient = require("./api/hueClient");
5+
const routes = require("./routes");
6+
7+
const port = 80;
8+
9+
axios.get("http://service.config/read/service.controller.hue")
10+
.then(rsp => {
11+
hueClient.host = rsp.data.hueBridge.host;
12+
hueClient.username = rsp.data.hueBridge.username;
13+
14+
return dao.fetchAllState();
15+
})
16+
.then(() => {
17+
const app = express();
18+
routes.register(app);
19+
app.listen(port, () => console.log(`Listening on port ${port}`));
20+
})
21+
.catch(err => {
22+
console.error("Error initialising service", err);
23+
});
24+
25+
26+
27+
28+
29+
30+
31+
32+

0 commit comments

Comments
 (0)