Skip to content

Commit 903efb8

Browse files
authored
feat: initial code (#1)
1 parent ace70b1 commit 903efb8

File tree

10 files changed

+555
-0
lines changed

10 files changed

+555
-0
lines changed

.gitignore

Whitespace-only changes.

.golangci.yml

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
run:
2+
tests: false
3+
timeout: 5m
4+
5+
linters-settings:
6+
cyclop:
7+
max-complexity: 20
8+
skip-tests: true
9+
funlen:
10+
lines: 80
11+
gofumpt:
12+
extra-rules: true
13+
14+
linters:
15+
enable-all: true
16+
disable:
17+
- interfacer # deprecated
18+
- scopelint # deprecated
19+
- maligned # deprecated
20+
- golint # deprecated
21+
- durationcheck
22+
- exhaustive
23+
- exhaustivestruct
24+
- forcetypeassert
25+
- funlen
26+
- gochecknoglobals
27+
- gochecknoinits
28+
- goerr113
29+
- gomnd
30+
- nlreturn
31+
- nilerr
32+
- noctx
33+
- tagliatelle
34+
- wrapcheck
35+
- wsl
36+
37+
issues:
38+
exclude-use-default: false
39+
exclude:
40+
- 'ST1000: at least one file in a package should have a package comment'
41+
exclude-rules:
42+
- path: module/client.go
43+
linters:
44+
- noctx

.readme/screenshot.png

3.95 KB
Loading

README.md

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
![Logo](http://svg.wiersma.co.za/glasslabs/module?title=HASS-FLOORPLAN&tag=a%20home%20assistant%20floorplan%20module)
2+
3+
Hass-floorplan is a home assistant floorplan module for [looking glass](http://github.com/glasslabs/looking-glass)
4+
5+
![Screenshot](.readme/screenshot.png)
6+
7+
## Usage
8+
9+
Clone the hass-floorplan into a path under your modules path and add the module path
10+
to under modules in your configuration.
11+
12+
```yaml
13+
modules:
14+
- name: floorplan
15+
path: github.com/glasslabs/hass-floorplan
16+
position: top:right
17+
config:
18+
url: http://my-hass-instance:8123/
19+
token: <your-hass-token>
20+
floorplan: {{ .ConfigPath }}/assets/floorplan.svg
21+
mapping:
22+
your-entity: your-svg-element
23+
```
24+
25+
## Configuration
26+
27+
### URL (url)
28+
29+
*Required*
30+
31+
Your home assistant instance URL, in the form of `http://home.assistant:8123/`.
32+
33+
### Token (token)
34+
35+
*Required*
36+
37+
You home assistant API token.
38+
39+
### Floorplan (floorplan)
40+
41+
*Required*
42+
43+
The path to your floor plan SVG file.
44+
45+
### Custom CSS (customCss)
46+
47+
*Optional*
48+
49+
The custom CSS file to style the SVG states. The element states are `on`, `off` and `unavailable`, although `off` should
50+
be the default state and not relied on to be set.
51+
52+
The default stylesheet uses classes `light` and `door` to style different elements.
53+
54+
### Entity Mapping (mapping)
55+
56+
*Optional*
57+
58+
An optional mapping from home assistant entity name to SVG element name.

assets/default.svg

+21
Loading

assets/style.css

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.hass-floorplan .light.on {
2+
fill: #ffd400;
3+
stroke: #ffd400;
4+
}
5+
6+
.hass-floorplan .light.unavailable {
7+
fill: #c00000;
8+
stroke: #c00000;
9+
}
10+
11+
.hass-floorplan .door.on {
12+
fill: #42a2dd;
13+
stroke: #42a2dd;
14+
}
15+
16+
.hass-floorplan .door.unavailable {
17+
fill: #c00000;
18+
stroke: #c00000;
19+
}

client.go

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package floorplan
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
)
12+
13+
// State is the state of an entity.
14+
type State struct {
15+
ID string
16+
State *bool
17+
}
18+
19+
// Client is a home assistant client.
20+
type Client struct {
21+
baseURL *url.URL
22+
token string
23+
client *http.Client
24+
}
25+
26+
// NewClient returns a home assistant client.
27+
func NewClient(baseURL *url.URL, token string) Client {
28+
return Client{
29+
baseURL: baseURL,
30+
token: token,
31+
client: &http.Client{},
32+
}
33+
}
34+
35+
type eventState struct {
36+
EntityID string `json:"entity_id"`
37+
State string `json:"state"`
38+
}
39+
40+
func stateToBool(state string) *bool {
41+
switch {
42+
case state == "on" || state == "open":
43+
return boolPtr(true)
44+
case state == "off" || state == "closed":
45+
return boolPtr(false)
46+
}
47+
48+
return nil
49+
}
50+
51+
func boolPtr(b bool) *bool {
52+
return &b
53+
}
54+
55+
// States returns the states of all entities.
56+
func (c Client) States(ctx context.Context) ([]State, error) {
57+
u, err := c.baseURL.Parse("api/states")
58+
if err != nil {
59+
return nil, fmt.Errorf("parsing state url: %w", err)
60+
}
61+
62+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
63+
if err != nil {
64+
return nil, fmt.Errorf("creating request: %w", err)
65+
}
66+
req.Header.Set("Authorization", "Bearer "+c.token)
67+
68+
resp, err := c.client.Do(req)
69+
if err != nil {
70+
return nil, fmt.Errorf("sending request: %w", err)
71+
}
72+
defer func() {
73+
_, _ = io.Copy(io.Discard, resp.Body)
74+
_ = resp.Body.Close()
75+
}()
76+
77+
if resp.StatusCode != 200 {
78+
return nil, fmt.Errorf("unexpected response code: %d", resp.StatusCode)
79+
}
80+
81+
var eventStates []eventState
82+
err = json.NewDecoder(resp.Body).Decode(&eventStates)
83+
if err != nil {
84+
return nil, fmt.Errorf("parsing response data: %w", err)
85+
}
86+
87+
states := make([]State, 0, len(eventStates))
88+
for _, state := range eventStates {
89+
states = append(states, State{
90+
ID: state.EntityID,
91+
State: stateToBool(state.State),
92+
})
93+
}
94+
95+
return states, nil
96+
}
97+
98+
type eventChanged struct {
99+
EventType string `json:"event_type"`
100+
Data struct {
101+
NewState eventState `json:"new_state"`
102+
} `json:"data"`
103+
}
104+
105+
// Events returns a stream of state changes.
106+
func (c Client) Events(ctx context.Context, done chan struct{}) (<-chan State, error) {
107+
u, err := c.baseURL.Parse("api/stream")
108+
if err != nil {
109+
return nil, fmt.Errorf("parsing state url: %w", err)
110+
}
111+
112+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
113+
if err != nil {
114+
return nil, fmt.Errorf("creating request: %w", err)
115+
}
116+
req.Header.Set("Authorization", "Bearer "+c.token)
117+
118+
resp, err := c.client.Do(req) //nolint: bodyclose
119+
if err != nil {
120+
return nil, fmt.Errorf("sending request: %w", err)
121+
}
122+
123+
if resp.StatusCode != 200 {
124+
return nil, fmt.Errorf("unexpected response code: %d", resp.StatusCode)
125+
}
126+
127+
ch := make(chan State, 10)
128+
go func() {
129+
defer func() {
130+
_ = resp.Body.Close()
131+
132+
close(ch)
133+
}()
134+
135+
buf := bufio.NewReader(resp.Body)
136+
for {
137+
line, err := buf.ReadBytes('\n')
138+
if err != nil {
139+
return
140+
}
141+
142+
if len(line) < 6 || string(line[:6]) != "data: " {
143+
continue
144+
}
145+
146+
if string(line[6:]) == "ping\n" {
147+
continue
148+
}
149+
150+
var event eventChanged
151+
err = json.Unmarshal(line[6:], &event)
152+
if err != nil {
153+
return
154+
}
155+
156+
if event.EventType != "state_changed" {
157+
continue
158+
}
159+
160+
ch <- State{
161+
ID: event.Data.NewState.EntityID,
162+
State: stateToBool(event.Data.NewState.State),
163+
}
164+
}
165+
}()
166+
167+
return ch, nil
168+
}

go.mod

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/glasslabs/floorplan
2+
3+
go 1.16
4+
5+
require github.com/glasslabs/looking-glass v0.1.0-alpha1

go.sum

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2+
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
3+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/glasslabs/looking-glass v0.1.0-alpha1 h1:cfqUVMSy0trnPDUqugS74ClF063zQ/IEN7sZyhIGLvs=
6+
github.com/glasslabs/looking-glass v0.1.0-alpha1/go.mod h1:uZHdBnmcltBmrCHBIrmoKxskSMAmli6/1lOcIS5PoMs=
7+
github.com/hamba/logger v1.0.1/go.mod h1:rpB9y29AN0sHvhc7QfzJZxgJFqh536/nZIFiVhqLkuE=
8+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9+
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
10+
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
11+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
12+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
13+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
14+
github.com/traefik/yaegi v0.9.2/go.mod h1:FAYnRlZyuVlEkvnkHq3bvJ1lW5be6XuwgLdkYgYG6Lk=
15+
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
16+
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
17+
github.com/zserge/lorca v0.1.9/go.mod h1:bVmnIbIRlOcoV285KIRSe4bUABKi7R7384Ycuum6e4A=
18+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
19+
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
20+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
21+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
22+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
24+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25+
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)