├── nodes ├── stampzilla-knx │ ├── RELEASE │ ├── setup.go │ ├── config.go │ └── main.go ├── stampzilla-spc │ ├── RELEASE │ └── main_test.go ├── stampzilla-1wire │ ├── RELEASE │ ├── config.example.json │ ├── config.go │ └── onewire │ │ └── 1wire.go ├── stampzilla-deconz │ ├── RELEASE │ ├── localconfig.json │ ├── README.md │ └── config.go ├── stampzilla-enocean │ ├── RELEASE │ └── handlersinit.go ├── stampzilla-exoline │ ├── RELEASE │ ├── lab │ │ ├── celltohex │ │ │ └── celltohex.go │ │ └── hextocell │ │ │ └── hextocell.go │ ├── config.go │ ├── exoline │ │ └── exoline_test.go │ └── config.example.json ├── stampzilla-mbus │ ├── RELEASE │ ├── config.go │ ├── config.example.json │ └── worker.go ├── stampzilla-modbus │ ├── RELEASE │ ├── config.go │ ├── decode_test.go │ ├── modbus.go │ └── config.example.json ├── stampzilla-server │ ├── RELEASE │ ├── web │ │ ├── .eslintignore │ │ ├── .gitignore │ │ ├── src │ │ │ ├── images │ │ │ │ ├── favicon.ico │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── mstile-150x150.png │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── gloomy-forest-background.jpg │ │ │ │ ├── browserconfig.xml │ │ │ │ └── site.webmanifest │ │ │ ├── components │ │ │ │ ├── editor.scss │ │ │ │ ├── CustomCheckbox.js │ │ │ │ ├── SocketModal.js │ │ │ │ ├── Wrapper.js │ │ │ │ ├── Card.js │ │ │ │ ├── FormModal.scss │ │ │ │ ├── ErrorBoundary.js │ │ │ │ ├── Link.js │ │ │ │ ├── Login.js │ │ │ │ └── Register.js │ │ │ ├── helpers.js │ │ │ ├── routes │ │ │ │ ├── dashboard │ │ │ │ │ ├── device.scss │ │ │ │ │ ├── HueColorPicker.js │ │ │ │ │ └── index.js │ │ │ │ └── automation │ │ │ │ │ └── components │ │ │ │ │ ├── Scene.scss │ │ │ │ │ ├── SavedStatePicker.scss │ │ │ │ │ └── StateEditor.js │ │ │ ├── middlewares │ │ │ │ ├── toast.js │ │ │ │ ├── rules.js │ │ │ │ ├── senders.js │ │ │ │ ├── schedules.js │ │ │ │ ├── savedstates.js │ │ │ │ ├── destinations.js │ │ │ │ └── persons.js │ │ │ ├── ducks │ │ │ │ ├── app.js │ │ │ │ ├── server.js │ │ │ │ ├── nodes.js │ │ │ │ ├── devices.js │ │ │ │ ├── requests.js │ │ │ │ ├── connections.js │ │ │ │ ├── certificates.js │ │ │ │ ├── index.js │ │ │ │ ├── savedstates.js │ │ │ │ ├── persons.js │ │ │ │ ├── senders.js │ │ │ │ ├── destinations.js │ │ │ │ ├── rules.js │ │ │ │ └── schedules.js │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── store.js │ │ ├── .babelrc │ │ └── .eslintrc │ ├── models │ │ ├── labels.go │ │ ├── notification │ │ │ ├── type.go │ │ │ ├── message.go │ │ │ ├── pushover │ │ │ │ └── pushover.go │ │ │ ├── email │ │ │ │ ├── email.go │ │ │ │ └── email_test.go │ │ │ ├── webhook │ │ │ │ ├── webhook_test.go │ │ │ │ └── webhook.go │ │ │ ├── file │ │ │ │ ├── file.go │ │ │ │ └── file_test.go │ │ │ ├── sender_test.go │ │ │ ├── wirepusher │ │ │ │ ├── wirepusher_test.go │ │ │ │ └── wirepusher.go │ │ │ └── destination_test.go │ │ ├── request.go │ │ ├── readyinfo.go │ │ ├── serverinfo.go │ │ ├── node_test.go │ │ ├── connection.go │ │ ├── node.go │ │ ├── message.go │ │ ├── persons │ │ │ └── person.go │ │ └── devices │ │ │ └── state.go │ ├── .gitignore │ ├── config.example.json │ ├── interfaces │ │ └── melody.go │ ├── README.md │ ├── store │ │ ├── senders.go │ │ ├── devices.go │ │ ├── certificates.go │ │ ├── server.go │ │ ├── logic.go │ │ ├── connections.go │ │ ├── server_test.go │ │ ├── destinations.go │ │ └── request.go │ ├── handlers │ │ └── websockethandler.go │ ├── main.go │ ├── logic │ │ ├── rules.json │ │ └── savedstate.go │ ├── helpers │ │ └── isprivateip.go │ └── websocket │ │ └── websocket.go ├── stampzilla-tibber │ ├── RELEASE │ ├── config.example.json │ ├── config.go │ └── tibber_test.go ├── stampzilla-zwave │ ├── RELEASE │ └── config.go ├── stampzilla-chromecast │ ├── RELEASE │ ├── state.go │ └── main.go ├── stampzilla-husdata-h60 │ ├── RELEASE │ ├── config.example.json │ ├── config.go │ ├── model_test.go │ └── main_test.go ├── stampzilla-nx-witness │ ├── RELEASE │ ├── config.example.json │ ├── model_test.go │ ├── config.go │ ├── model.go │ └── api.go ├── stampzilla-google-assistant │ ├── RELEASE │ ├── device.go │ ├── config.go │ ├── README.md │ ├── oauthhandler.go │ └── googleassistant │ │ ├── request.go │ │ └── response.go ├── stampzilla-metrics-influxdb │ └── RELEASE ├── stampzilla-magicmirror │ ├── web │ │ ├── .prettierignore │ │ ├── now.json │ │ ├── .prettierrc │ │ ├── renovate.json │ │ ├── src │ │ │ ├── setupProxy.js │ │ │ ├── routes │ │ │ │ ├── app │ │ │ │ │ └── index.js │ │ │ │ └── home │ │ │ │ │ ├── Clock.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── DeviceList.js │ │ │ ├── ducks │ │ │ │ ├── index.js │ │ │ │ ├── config.js │ │ │ │ ├── devices.js │ │ │ │ └── connection.js │ │ │ ├── index.js │ │ │ ├── .eslintrc │ │ │ └── store.js │ │ ├── .gitignore │ │ ├── public │ │ │ ├── manifest.json │ │ │ └── index.html │ │ ├── README.md │ │ ├── LICENSE │ │ └── package.json │ ├── config.go │ └── example-config.json ├── stampzilla-linux │ ├── RELEASE │ ├── health.go │ ├── main.go │ ├── volume.go │ └── dpms.go ├── stampzilla-streamdeck │ ├── fonts │ │ └── Robotosr.ttf │ ├── config.go │ └── keys.go ├── stampzilla-telldus │ ├── README.md │ ├── README │ └── main.c ├── stampzilla-nibe │ ├── config.go │ └── nibe │ │ └── parameters.go └── stampzilla-keba-p30 │ └── types.go ├── hooks ├── pre-commit └── install-hooks ├── docs ├── screenshots │ ├── debug.png │ ├── nodes.png │ ├── security.png │ ├── automation.png │ └── dashboard.png ├── screenshots.md ├── devices.md └── README.md ├── pkg ├── hueemulator │ ├── huestate.go │ └── request.go ├── runner │ └── runner.go ├── types │ ├── duration.go │ └── duration_test.go ├── installer │ ├── prepare.go │ ├── installer.go │ ├── pidfile.go │ ├── binary │ │ └── releases.go │ └── user.go ├── build │ └── build.go └── node │ └── mdns.go ├── cmd └── stampzilla │ └── Makefile ├── .gitignore ├── .travis.yml ├── coverage ├── Makefile └── README.md /nodes/stampzilla-knx/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-spc/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-1wire/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-deconz/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-enocean/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-exoline/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-mbus/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-modbus/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-tibber/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-zwave/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-chromecast/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-husdata-h60/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-nx-witness/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-google-assistant/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/stampzilla-metrics-influxdb/RELEASE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | make test 4 | -------------------------------------------------------------------------------- /nodes/stampzilla-google-assistant/device.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/.prettierignore: -------------------------------------------------------------------------------- 1 | package.json -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/.eslintignore: -------------------------------------------------------------------------------- 1 | src/index.scss 2 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | stats.json 3 | -------------------------------------------------------------------------------- /nodes/stampzilla-1wire/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "interval":"30s" 3 | } 4 | -------------------------------------------------------------------------------- /nodes/stampzilla-deconz/localconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "APIKey": "0342FBAE6F" 3 | } 4 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/labels.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Labels map[string]string 4 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "create-react-app-redux.now.sh" 3 | } 4 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.key 3 | vendor 4 | configs 5 | 6 | web/dist 7 | *.json 8 | -------------------------------------------------------------------------------- /docs/screenshots/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/debug.png -------------------------------------------------------------------------------- /docs/screenshots/nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/nodes.png -------------------------------------------------------------------------------- /nodes/stampzilla-linux/RELEASE: -------------------------------------------------------------------------------- 1 | { 2 | "cgo": true, 3 | "arch": [ 4 | "amd64" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/screenshots/security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/security.png -------------------------------------------------------------------------------- /nodes/stampzilla-tibber/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "carChargeDuration": "6h", 3 | "token": "asdf" 4 | } 5 | -------------------------------------------------------------------------------- /docs/screenshots/automation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/automation.png -------------------------------------------------------------------------------- /docs/screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/dashboard.png -------------------------------------------------------------------------------- /nodes/stampzilla-husdata-h60/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "interval":"30s", 4 | "host":"http://192.168.13.181/" 5 | } 6 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "jsxBracketSameLine": true 5 | } -------------------------------------------------------------------------------- /nodes/stampzilla-streamdeck/fonts/Robotosr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-streamdeck/fonts/Robotosr.ttf -------------------------------------------------------------------------------- /hooks/install-hooks: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ROOT_FOLDER=`git rev-parse --show-toplevel` 4 | 5 | cp -v $ROOT_FOLDER/hooks/pre-commit $ROOT_FOLDER/.git/hooks 6 | 7 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/favicon.ico -------------------------------------------------------------------------------- /pkg/hueemulator/huestate.go: -------------------------------------------------------------------------------- 1 | package hueemulator 2 | 3 | type huestate struct { 4 | Handler Handler 5 | // OnState bool 6 | Light *light 7 | Id int 8 | } 9 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": "8080", 3 | "tlsPort": "6443", 4 | "uuid": "serveruuid", 5 | "name": "stampzilla server" 6 | } 7 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/favicon-16x16.png -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/favicon-32x32.png -------------------------------------------------------------------------------- /nodes/stampzilla-zwave/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Device string `json:"device"` 5 | RecordToFile string `json:"recordToFile"` 6 | } 7 | -------------------------------------------------------------------------------- /nodes/stampzilla-nx-witness/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "http://1.1.1.1:7001", 3 | "username": "admin", 4 | "password": "asdf", 5 | "interval": "5m" 6 | } 7 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/apple-touch-icon.png -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/mstile-150x150.png -------------------------------------------------------------------------------- /nodes/stampzilla-1wire/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Interval string 5 | } 6 | 7 | func NewConfig() *Config { 8 | return &Config{} 9 | } 10 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/type.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | type Type string 4 | 5 | const ( 6 | TypeMail Type = "mail" 7 | TypeSms Type = "sms" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/hueemulator/request.go: -------------------------------------------------------------------------------- 1 | package hueemulator 2 | 3 | type Request struct { 4 | UserId string 5 | // RequestedOnState bool 6 | Request *request 7 | RemoteAddr string 8 | } 9 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/editor.scss: -------------------------------------------------------------------------------- 1 | .editor { 2 | padding: 6px; 3 | .node { 4 | color: #000; 5 | background: #eee; 6 | padding: 2px 2px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/gloomy-forest-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/gloomy-forest-background.jpg -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | }, 7 | "reviewers": ["notrab"] 8 | } 9 | -------------------------------------------------------------------------------- /nodes/stampzilla-husdata-h60/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Interval string 5 | Host string 6 | } 7 | 8 | func NewConfig() *Config { 9 | return &Config{} 10 | } 11 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/helpers.js: -------------------------------------------------------------------------------- 1 | 2 | let lastId = 0; 3 | export const uniqeId = (prefix = 'id') => { 4 | lastId += 1; 5 | return `${prefix}${lastId}`; 6 | }; 7 | 8 | export default false; 9 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Request struct { 4 | Version string `json:"version,omitempty"` 5 | Type string `json:"type,omitempty"` 6 | CSR string `json:"csr"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | type Runner interface { 4 | Start(nodes ...string) error 5 | Stop(nodes ...string) error 6 | Restart(nodes ...string) error 7 | Status() error 8 | Close() 9 | } 10 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/routes/dashboard/device.scss: -------------------------------------------------------------------------------- 1 | .offline { 2 | color: #999; 3 | } 4 | 5 | .device { 6 | div[role="button"] { 7 | cursor: pointer; 8 | border: 0; 9 | outline: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /nodes/stampzilla-telldus/README.md: -------------------------------------------------------------------------------- 1 | # telldus 2 | 3 | Connects to local telldusd using telldus C API 4 | 5 | ## Configuration 6 | 7 | telldus requires no extra config except what is configured in /etc/tellstick.conf for telldusd to work. 8 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const proxy = require("http-proxy-middleware") 2 | 3 | module.exports = app => { 4 | app.use(proxy("/ws", {target: "http://localhost:8089", ws: true})) 5 | app.use(proxy("/proxy", {target: "http://localhost:8089"})) 6 | } 7 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/readyinfo.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/persons" 4 | 5 | type ReadyInfo struct { 6 | Method string `json:"method"` 7 | User *persons.Person `json:"user"` 8 | } 9 | -------------------------------------------------------------------------------- /nodes/stampzilla-nx-witness/model_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStampzillaDeviceID(t *testing.T) { 10 | r := Rule{ 11 | Comment: "test stampzilla:123a", 12 | } 13 | assert.Equal(t, "123a", r.StampzillaDeviceID()) 14 | } 15 | -------------------------------------------------------------------------------- /nodes/stampzilla-telldus/README: -------------------------------------------------------------------------------- 1 | The telldus c lib is needed for compilation and the search paths is not set correctly by default. 2 | 3 | Here is a dirty workaround 4 | 5 | ln -s /usr/local/include/telldus/telldus-core/client/telldus-core.h /usr/include/telldus-core.h 6 | ln -s /usr/lib/libtelldus-core.so.2 /usr/lib/libtelldus-core.so 7 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/routes/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router-dom' 3 | import Home from '../home' 4 | 5 | const App = () => ( 6 |
7 |
8 | 9 |
10 |
11 | ) 12 | 13 | export default App 14 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/serverinfo.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ServerInfo struct { 4 | Name string `json:"name"` 5 | UUID string `json:"uuid"` 6 | TLSPort string `json:"tlsPort"` 7 | Port string `json:"port"` 8 | Init bool `json:"init"` 9 | AllowLogin bool `json:"allowLogin"` 10 | } 11 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /nodes/stampzilla-nx-witness/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | type Config struct { 6 | Username string `json:"username"` 7 | Password string `json:"password"` 8 | Host string `json:"host"` 9 | Interval string `json:"interval"` 10 | interval time.Duration 11 | } 12 | 13 | func NewConfig() *Config { 14 | return &Config{} 15 | } 16 | -------------------------------------------------------------------------------- /nodes/stampzilla-tibber/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | type Config struct { 6 | CarChargeDuration string `json:"carChargeDuration"` 7 | carChargeDuration time.Duration 8 | Token string `json:"token"` 9 | HomeID string `json:"homeId"` 10 | } 11 | 12 | func NewConfig() *Config { 13 | return &Config{} 14 | } 15 | -------------------------------------------------------------------------------- /docs/screenshots.md: -------------------------------------------------------------------------------- 1 | # Screenshots of the web gui 2 | 3 | Dashboard 4 | ![](./screenshots/dashboard.png?raw=true) 5 | 6 | Automation 7 | ![](./screenshots/automation.png?raw=true) 8 | 9 | Nodes 10 | ![](./screenshots/nodes.png?raw=true) 11 | 12 | Security 13 | ![](./screenshots/security.png?raw=true) 14 | 15 | Debug 16 | ![](./screenshots/debug.png?raw=true) 17 | -------------------------------------------------------------------------------- /nodes/stampzilla-google-assistant/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Port string `json:"port"` 5 | ClientID string `json:"clientID"` 6 | ClientSecret string `json:"clientSecret"` 7 | ProjectID string `json:"projectID"` 8 | APIKey string `json:"APIKey"` 9 | } 10 | 11 | func NewConfig() *Config { 12 | return &Config{} 13 | } 14 | -------------------------------------------------------------------------------- /nodes/stampzilla-modbus/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Registers Registers 5 | Device string 6 | } 7 | 8 | func NewConfig() *Config { 9 | return &Config{ 10 | Registers: make(Registers), 11 | } 12 | } 13 | 14 | type Register struct { 15 | Name string 16 | Id uint16 17 | Value interface{} 18 | Base int64 19 | } 20 | 21 | type Registers map[string]*Register 22 | -------------------------------------------------------------------------------- /cmd/stampzilla/Makefile: -------------------------------------------------------------------------------- 1 | LDFLAGS=-ldflags "-X main.buildstamp \"`date -u '+%Y-%m-%d %H:%M:%S %Z'`\" -X main.githash `git rev-parse --short HEAD`" 2 | 3 | all: 4 | go build ${LDFLAGS} $(filter-out $@,$(MAKECMDGOALS)) 5 | 6 | install: 7 | go install ${LDFLAGS} $(filter-out $@,$(MAKECMDGOALS)) 8 | 9 | run: 10 | go build ${LDFLAGS} 11 | ./stampzilla $(filter-out $@,$(MAKECMDGOALS)) 12 | 13 | %: 14 | @: 15 | -------------------------------------------------------------------------------- /nodes/stampzilla-deconz/README.md: -------------------------------------------------------------------------------- 1 | # deconz 2 | 3 | Connects to a running deconz instance and can then control zigbee devices like IKEA trådfri and xiaomi sensors etc. 4 | 5 | ## Configuration 6 | 7 | It requires the password to the web interface and then automaticly creates a API user. 8 | ``` 9 | { 10 | "ip": "192.168.13.1", 11 | "port": "9042", 12 | "password": "deconz password" 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/message.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models" 4 | 5 | type Messages []Message 6 | 7 | type Message struct { 8 | DestinationSelector models.Labels `json:"destinationSelector"` 9 | Head string `json:"head"` 10 | Body string `json:"body"` 11 | } 12 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/node_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSetAlias(t *testing.T) { 11 | n := &Node{} 12 | id, _ := devices.NewIDFromString("1.1") 13 | n.SetAlias(id, "alias") 14 | assert.Equal(t, "alias", n.Alias(id)) 15 | } 16 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/connection.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/lesismal/melody" 4 | 5 | type Connection struct { 6 | Type string `json:"type"` 7 | RemoteAddr string `json:"remoteAddr"` 8 | NodeUuid string `json:"nodeUuid,omitEmpty"` 9 | Attributes map[string]interface{} `json:"attributes"` 10 | 11 | Session *melody.Session `json:"-"` 12 | } 13 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React + Redux App", 3 | "name": "Create React App Redux", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/ducks/index.js: -------------------------------------------------------------------------------- 1 | import forecast from './forecast' 2 | import devices from './devices' 3 | import config from './config' 4 | import { connectRouter } from 'connected-react-router/immutable' 5 | import { combineReducers } from 'redux-immutable' 6 | 7 | export default history => 8 | combineReducers({ 9 | forecast, 10 | devices, 11 | config, 12 | router: connectRouter(history) 13 | }) 14 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Weather struct { 5 | Current struct { 6 | Temperature struct { 7 | Device string `json:"device"` 8 | Field string `json:"field"` 9 | } `json:"temperature"` 10 | Humidity struct { 11 | Device string `json:"device"` 12 | Field string `json:"field"` 13 | } `json:"humidity"` 14 | } `json:"current"` 15 | } `json:"weather"` 16 | } 17 | -------------------------------------------------------------------------------- /nodes/stampzilla-streamdeck/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "sync" 4 | 5 | type config struct { 6 | Pages map[string]page `json:"pages"` 7 | sync.Mutex 8 | } 9 | 10 | type page struct { 11 | Keys [15]key `json:"keys"` 12 | } 13 | 14 | type key struct { 15 | Name string `json:"name"` 16 | Node string `json:"node"` 17 | Device string `json:"device"` 18 | Action string `json:"action"` 19 | Field string `json:"field"` 20 | } 21 | -------------------------------------------------------------------------------- /nodes/stampzilla-exoline/lab/celltohex/celltohex.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | ) 9 | 10 | func main() { 11 | intVar, err := strconv.Atoi(os.Args[1]) 12 | if err != nil { 13 | log.Panic(err) 14 | return 15 | } 16 | 17 | fmt.Println("celltoHex", cellToHex(intVar)) 18 | } 19 | 20 | func cellToHex(c int) string { 21 | return fmt.Sprintf("%x", []byte{byte(c / 60), byte(c % 60)}) 22 | } 23 | -------------------------------------------------------------------------------- /nodes/stampzilla-exoline/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Interval string 5 | Host string 6 | Variables []Variables 7 | } 8 | 9 | type Variables struct { 10 | Name string `json:"name"` 11 | LoadNumber int `json:"loadNumber"` 12 | Cell int `json:"cell"` 13 | Type string `json:"type"` 14 | Write bool `json:"write"` 15 | } 16 | 17 | func NewConfig() *Config { 18 | return &Config{} 19 | } 20 | -------------------------------------------------------------------------------- /nodes/stampzilla-exoline/lab/hextocell/hextocell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | fmt.Println("hexToCell", hexToCell(os.Args[1])) 12 | } 13 | func hexToCell(s string) int { 14 | v, err := hex.DecodeString(s) 15 | if err != nil { 16 | log.Println(err) 17 | return 0 18 | } 19 | first := int(v[0]) 20 | second := int(v[1]) 21 | return first*60 + second 22 | } 23 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/middlewares/toast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toast } from 'react-toastify'; 3 | 4 | const toastify = (store) => (next) => async (action) => { 5 | try { 6 | return await next(action); 7 | } catch (ex) { 8 | toast.error( 9 | <> 10 | Save failed: 11 | {' '} 12 | {ex} 13 | , 14 | ); 15 | 16 | throw ex; 17 | } 18 | }; 19 | 20 | export default toastify; 21 | -------------------------------------------------------------------------------- /nodes/stampzilla-exoline/exoline/exoline_test.go: -------------------------------------------------------------------------------- 1 | package exoline 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFloat64bytes(t *testing.T) { 10 | f := 12.34 11 | b := FloatTobytes(f) 12 | assert.Equal(t, []byte{164, 112, 69, 65}, b) 13 | } 14 | 15 | func TestAsRoundedFloat(t *testing.T) { 16 | b := []byte{164, 112, 69, 65} 17 | f, err := AsRoundedFloat(b) 18 | assert.NoError(t, err) 19 | assert.Equal(t, 12.34, f) 20 | } 21 | -------------------------------------------------------------------------------- /nodes/stampzilla-tibber/tibber_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | func TestWS(t *testing.T) { 5 | 6 | u, err := getWsURL("token", "homeid") 7 | assert.NoError(t, err) 8 | fmt.Println("url is", u) 9 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) 10 | defer cancel() 11 | err = reconnectWS(ctx, u, "token", "homeid", func(data *DataPayload) { 12 | fmt.Println("the data is") 13 | spew.Dump(data) 14 | }) 15 | assert.NoError(t, err) 16 | } 17 | */ 18 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/interfaces/melody.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | type MelodyWriter interface { 4 | Write(msg []byte) error 5 | } 6 | 7 | type MelodySession interface { 8 | MelodyWriter 9 | Close() error 10 | CloseWithMsg(msg []byte) error 11 | Get(key string) (value interface{}, exists bool) 12 | IsClosed() bool 13 | // MustGet(key string) interface{} // removed in github.com/lesismal/melody fork 14 | Set(key string, value interface{}) 15 | WriteBinary(msg []byte) error 16 | } 17 | -------------------------------------------------------------------------------- /nodes/stampzilla-modbus/decode_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDecode(t *testing.T) { 10 | data1 := []byte{0xff, 0xff} // -0.1 11 | data2 := []byte{0xff, 0xfa} // -0.6 12 | data3 := []byte{0x00, 0x3c} // 6 13 | data4 := []byte{0x00, 0x02} // 0.2 14 | 15 | assert.Equal(t, -0.1, decode(data1)/10) 16 | assert.Equal(t, -0.6, decode(data2)/10) 17 | assert.Equal(t, 6.0, decode(data3)/10) 18 | assert.Equal(t, 0.2, decode(data4)/10) 19 | } 20 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/README.md: -------------------------------------------------------------------------------- 1 | # Server 2 | 3 | The server is the main component. It has the following responsibilities among other things. 4 | 5 | * Stores the config for all the nodes and sent it to them when they start. 6 | * Serves a web gui. 7 | * Stores and evalutate rules. 8 | * Stores and runs schedules. 9 | 10 | 11 | ### Developing 12 | 13 | Install deps 14 | 15 | ``` 16 | dep ensure -vendor-only 17 | 18 | ``` 19 | 20 | Allow https to localhost in chrome 21 | ``` 22 | chrome://flags/#allow-insecure-localhost 23 | ``` 24 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/middlewares/rules.js: -------------------------------------------------------------------------------- 1 | import { write } from '../components/Websocket'; 2 | 3 | const rules = store => next => (action) => { 4 | const prev = store.getState().getIn(['rules', 'list']); 5 | const result = next(action); 6 | const after = store.getState().getIn(['rules', 'list']); 7 | 8 | if (!after.equals(prev) && action.type !== 'rules_UPDATE') { 9 | write({ 10 | type: 'update-rules', 11 | body: after.toJS(), 12 | }); 13 | } 14 | return result; 15 | }; 16 | 17 | export default rules; 18 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/routes/automation/components/Scene.scss: -------------------------------------------------------------------------------- 1 | .saved-state-builder { 2 | .menu { 3 | display: flex; 4 | background: #eee; 5 | padding: 6px 14px; 6 | margin-left: -16px; 7 | margin-right: -16px; 8 | } 9 | 10 | .devices { 11 | display: flex; 12 | flex-direction: column; 13 | 14 | color: #999; 15 | 16 | .trait { 17 | filter: blur(2px); 18 | } 19 | .selected { 20 | color: #000; 21 | 22 | .trait { 23 | filter: none; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/senders.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/notification" 4 | 5 | func (store *Store) GetSenders() map[string]notification.Sender { 6 | store.RLock() 7 | defer store.RUnlock() 8 | return store.Senders.All() 9 | } 10 | 11 | func (store *Store) AddOrUpdateSender(sender notification.Sender) { 12 | if sender.UUID == "" { 13 | return 14 | } 15 | 16 | store.Senders.Add(sender) 17 | store.Senders.Save("senders.json") 18 | store.runCallbacks("senders") 19 | } 20 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/middlewares/senders.js: -------------------------------------------------------------------------------- 1 | import { write } from '../components/Websocket'; 2 | 3 | const senders = (store) => (next) => (action) => { 4 | const prev = store.getState().getIn(['senders', 'list']); 5 | const result = next(action); 6 | const after = store.getState().getIn(['senders', 'list']); 7 | 8 | if (!after.equals(prev) && action.type !== 'senders_UPDATE') { 9 | write({ 10 | type: 'update-senders', 11 | body: after.toJS(), 12 | }); 13 | } 14 | return result; 15 | }; 16 | 17 | export default senders; 18 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/handlers/websockethandler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/interfaces" 8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models" 9 | ) 10 | 11 | type WebsocketHandler interface { 12 | Message(s interfaces.MelodySession, msg *models.Message) (json.RawMessage, error) 13 | Connect(s interfaces.MelodySession, r *http.Request, keys map[string]interface{}) error 14 | Disconnect(s interfaces.MelodySession) error 15 | } 16 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/middlewares/schedules.js: -------------------------------------------------------------------------------- 1 | import { write } from '../components/Websocket'; 2 | 3 | const schedules = store => next => (action) => { 4 | const prev = store.getState().getIn(['schedules', 'list']); 5 | const result = next(action); 6 | const after = store.getState().getIn(['schedules', 'list']); 7 | 8 | if (!after.equals(prev) && action.type !== 'schedules_UPDATE') { 9 | write({ 10 | type: 'update-schedules', 11 | body: after.toJS(), 12 | }); 13 | } 14 | return result; 15 | }; 16 | 17 | export default schedules; 18 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/middlewares/savedstates.js: -------------------------------------------------------------------------------- 1 | import { write } from '../components/Websocket'; 2 | 3 | const savedstates = store => next => (action) => { 4 | const prev = store.getState().getIn(['savedstates', 'list']); 5 | const result = next(action); 6 | const after = store.getState().getIn(['savedstates', 'list']); 7 | 8 | if (!after.equals(prev) && action.type !== 'savedstates_UPDATE') { 9 | write({ 10 | type: 'update-savedstates', 11 | body: after.toJS(), 12 | }); 13 | } 14 | return result; 15 | }; 16 | 17 | export default savedstates; 18 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/middlewares/destinations.js: -------------------------------------------------------------------------------- 1 | import { write } from '../components/Websocket'; 2 | 3 | const senders = (store) => (next) => (action) => { 4 | const prev = store.getState().getIn(['destinations', 'list']); 5 | const result = next(action); 6 | const after = store.getState().getIn(['destinations', 'list']); 7 | 8 | if (!after.equals(prev) && action.type !== 'destinations_UPDATE') { 9 | write({ 10 | type: 'update-destinations', 11 | body: after.toJS(), 12 | }); 13 | } 14 | return result; 15 | }; 16 | 17 | export default senders; 18 | -------------------------------------------------------------------------------- /pkg/types/duration.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type Duration time.Duration 10 | 11 | func (d Duration) String() string { 12 | return time.Duration(d).String() 13 | } 14 | 15 | func (d Duration) MarshalJSON() (b []byte, err error) { 16 | return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil 17 | } 18 | 19 | func (d *Duration) UnmarshalJSON(b []byte) error { 20 | td, err := time.ParseDuration(strings.Trim(string(b), `"`)) 21 | if err != nil { 22 | return err 23 | } 24 | *d = Duration(td) 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/images/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stampzilla-go", 3 | "short_name": "stampzilla", 4 | "icons": [ 5 | { 6 | "src": "/images/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/images/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /nodes/stampzilla-google-assistant/README.md: -------------------------------------------------------------------------------- 1 | # google-assistant 2 | 3 | Exposes all the devices in the server for control in google assistant (google home etc) 4 | 5 | ## Configuration 6 | 7 | The port is what port to listen to for google actions API calls. That port must be avilable from the internet. 8 | You need to create a project in google actions console: https://developers.google.com/actions/smarthome/create#create-project 9 | And fill in the values in the config below. 10 | 11 | ``` 12 | { 13 | "port": "8000", 14 | "clientID": "", 15 | "clientSecret": "", 16 | "projectID": "", 17 | "APIKey": "" 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /pkg/installer/prepare.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | func Prepare() error { 4 | // Make sure our infrastructure is correct 5 | //// Create required user and folders 6 | CreateUser("stampzilla") 7 | CreateDirAsUser("/var/spool/stampzilla", "stampzilla") 8 | CreateDirAsUser("/var/log/stampzilla", "stampzilla") 9 | CreateDirAsUser("/home/stampzilla", "stampzilla") 10 | CreateDirAsUser("/home/stampzilla/go", "stampzilla") 11 | CreateDirAsUser("/home/stampzilla/go/bin", "stampzilla") 12 | CreateDirAsUser("/etc/stampzilla", "stampzilla") 13 | CreateDirAsUser("/etc/stampzilla/nodes", "stampzilla") 14 | 15 | c := Config{} 16 | c.CreateConfig() 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/app.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'app', 6 | ['UPDATE'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | url: `${window.location.protocol.match(/^https/) ? 'wss' : 'ws'}://${window.location.host}/ws`, 11 | }); 12 | 13 | export const update = state => ( 14 | { type: c.UPDATE, state } 15 | ); 16 | 17 | export default function reducer(state = defaultState, action) { 18 | switch (action.type) { 19 | case c.UPDATE: { 20 | return state 21 | .mergeDeep(action.state); 22 | } 23 | default: { 24 | return state; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/example-config.json: -------------------------------------------------------------------------------- 1 | { 2 | widgets: [ 3 | { 4 | type: 'clock' 5 | }, 6 | { 7 | type: 'devicelist', 8 | devices: [ 9 | { 10 | title: 'Temperatur', 11 | device: 'dfc28c72-0e5c-451b-add2-c99af57510c8.1', 12 | state: 'on', 13 | states: { 14 | true: 'Till', 15 | false: 'Från' 16 | } 17 | }, 18 | { 19 | title: 'Luftfuktighet', 20 | device: 'dfc28c72-0e5c-451b-add2-c99af57510c8.2', 21 | state: 'on', 22 | unit: 'grader' 23 | } 24 | ] 25 | }, 26 | { 27 | type: 'forecast' 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /pkg/installer/installer.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/stampzilla/stampzilla-go/v2/pkg/installer/binary" 7 | "github.com/stampzilla/stampzilla-go/v2/pkg/installer/source" 8 | ) 9 | 10 | type Installer interface { 11 | Prepare() error 12 | Install(...string) error 13 | Update(...string) error 14 | } 15 | 16 | type InstallSource uint8 17 | 18 | const ( 19 | Binaries = iota 20 | SourceCode 21 | ) 22 | 23 | func New(s InstallSource) (Installer, error) { 24 | switch s { 25 | case Binaries: 26 | return binary.NewInstaller(), nil 27 | case SourceCode: 28 | return source.NewInstaller(), nil 29 | } 30 | return nil, fmt.Errorf("No installer with value %d is available", s) 31 | } 32 | -------------------------------------------------------------------------------- /nodes/stampzilla-nibe/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-nibe/nibe" 8 | ) 9 | 10 | type Config struct { 11 | Port string `json:"port"` 12 | } 13 | 14 | func updatedConfig(n *nibe.Nibe) func(data json.RawMessage) error { 15 | return func(data json.RawMessage) error { 16 | logrus.Info("Received config from server:", string(data)) 17 | 18 | newConf := &Config{} 19 | err := json.Unmarshal(data, newConf) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | config = newConf 25 | logrus.Info("Config is now: ", config) 26 | 27 | n.Connect(config.Port) 28 | 29 | return nil 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/middlewares/persons.js: -------------------------------------------------------------------------------- 1 | import { request } from '../components/Websocket'; 2 | import reducer from '../ducks/persons'; 3 | 4 | const rules = (store) => (next) => async (action) => { 5 | if (action.type.startsWith('persons_') && !action.type.endsWith('_UPDATE')) { 6 | // Generate a updated state by running the reducer 7 | const updatedState = reducer(store.getState().get('persons'), action); 8 | 9 | // Try to save 10 | await request({ 11 | type: 'update-persons', 12 | body: updatedState.get('list').toJS(), 13 | }); 14 | 15 | // Jump to the next middleware 16 | return next(action); 17 | } 18 | 19 | return next(action); 20 | }; 21 | 22 | export default rules; 23 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/routes/dashboard/HueColorPicker.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { CustomPicker } from 'react-color'; 3 | import { Hue } from 'react-color/lib/components/common'; 4 | import SliderPointer from 'react-color/lib/components/slider/SliderPointer'; 5 | 6 | class HueColorPicker extends PureComponent { 7 | render() { 8 | return ( 9 |
10 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default CustomPicker(HueColorPicker); 22 | -------------------------------------------------------------------------------- /pkg/build/build.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import "fmt" 4 | 5 | // Version is used by CI/CD system to set the version of the built binary. 6 | var Version = "dev" 7 | 8 | // BuildTime is the time of this build. 9 | var BuildTime = "" 10 | 11 | // Commit contains the SHA commit hash. 12 | var Commit = "" 13 | 14 | // String returns the version info as a string. 15 | func String() string { 16 | return fmt.Sprintf(`Version: "%s", BuildTime: "%s", Commit: "%s" `, Version, BuildTime, Commit) 17 | } 18 | 19 | func JSON() string { 20 | return fmt.Sprintf(`{"Version": "%s", "BuildTime": "%s", "Commit": "%s"} `, Version, BuildTime, Commit) 21 | } 22 | 23 | type Data struct { 24 | Version string 25 | BuildTime string 26 | Commit string 27 | } 28 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { ConnectedRouter } from 'connected-react-router/immutable' 5 | import store, { history } from './store' 6 | import App from './routes/app' 7 | import Websocket from './components/Websocket' 8 | 9 | import 'sanitize.css/sanitize.css' 10 | import './index.css' 11 | 12 | const target = document.querySelector('#root') 13 | 14 | render( 15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
, 23 | target 24 | ) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.key 3 | 4 | #Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | 27 | nodes/stampzilla-server/public/bower_components/ 28 | stampzilla-server/gin-bin 29 | 30 | # application specifics 31 | config.json 32 | devices.json 33 | 34 | dist 35 | 36 | #vim stuff 37 | # swap 38 | [._]*.s[a-w][a-z] 39 | [._]s[a-w][a-z] 40 | # session 41 | Session.vim 42 | # temporary 43 | .netrwhist 44 | *~ 45 | # auto-generated tag files 46 | tags 47 | coverage.txt 48 | -------------------------------------------------------------------------------- /nodes/stampzilla-mbus/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Config struct { 4 | Interval string `json:"interval"` 5 | Host string `json:"host"` 6 | Port string `json:"port"` 7 | Devices []Device `json:"devices"` 8 | } 9 | 10 | type Device struct { 11 | Interval string `json:"interval"` 12 | Enabled bool `json:"enabled"` 13 | Name string `json:"name"` 14 | PrimaryAddress int `json:"primaryAddress"` 15 | // Frames which frames and datarecord to fetch. 16 | // Only 0 (first frame) is supported for now 17 | Frames map[string][]Record `json:"frames"` 18 | } 19 | 20 | type Record struct { 21 | Id int `json:"id"` 22 | Name string `json:"name"` 23 | } 24 | 25 | func NewConfig() *Config { 26 | return &Config{} 27 | } 28 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/ducks/config.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable' 2 | import { defineAction } from 'redux-define' 3 | 4 | const c = defineAction('config', ['UPDATE']) 5 | 6 | const defaultState = Map({}) 7 | 8 | // Actions 9 | export function update(config) { 10 | return { type: c.UPDATE, config } 11 | } 12 | 13 | // Subscribe to channels and register the action for the packages 14 | export function subscribe(dispatch) { 15 | return { 16 | config: config => dispatch(update(config)) 17 | } 18 | } 19 | 20 | // Reducer 21 | export default function reducer(state = defaultState, action) { 22 | switch (action.type) { 23 | case c.UPDATE: { 24 | return fromJS(action.config) 25 | } 26 | default: 27 | return state 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react", 10 | ], 11 | "plugins": [ 12 | "react-hot-loader/babel", 13 | "@babel/plugin-syntax-dynamic-import", 14 | "@babel/plugin-syntax-import-meta", 15 | "@babel/plugin-proposal-class-properties", 16 | "@babel/plugin-proposal-json-strings", 17 | [ 18 | "@babel/plugin-proposal-decorators", 19 | { 20 | "legacy": true 21 | } 22 | ], 23 | "@babel/plugin-proposal-function-sent", 24 | "@babel/plugin-proposal-export-namespace-from", 25 | "@babel/plugin-proposal-numeric-separator", 26 | "@babel/plugin-proposal-throw-expressions" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/server.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction('server', ['UPDATE']); 5 | 6 | const defaultState = Map({}); 7 | 8 | export const update = state => (dispatch, getState) => { 9 | if ( 10 | !getState() 11 | .get('server') 12 | .equals( 13 | getState() 14 | .get('server') 15 | .mergeDeep(state), 16 | ) 17 | ) { 18 | dispatch({ type: c.UPDATE, state }); 19 | } 20 | }; 21 | 22 | export default function reducer(state = defaultState, action) { 23 | switch (action.type) { 24 | case c.UPDATE: { 25 | return state.mergeDeep(action.state); 26 | } 27 | default: { 28 | return state; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/types/duration_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMarshalJSOn(t *testing.T) { 12 | s := struct { 13 | D Duration 14 | }{ 15 | D: Duration(time.Second), 16 | } 17 | 18 | j, err := json.Marshal(&s) 19 | assert.NoError(t, err) 20 | assert.Equal(t, `{"D":"1s"}`, string(j)) 21 | } 22 | 23 | func TestUnmarshalJSON(t *testing.T) { 24 | s := struct { 25 | D Duration 26 | }{} 27 | 28 | err := json.Unmarshal([]byte(`{"D":"1s"}`), &s) 29 | assert.NoError(t, err) 30 | assert.Equal(t, Duration(time.Second), s.D) 31 | 32 | err = json.Unmarshal([]byte(`{"D":"200ms"}`), &s) 33 | assert.NoError(t, err) 34 | assert.Equal(t, Duration(time.Millisecond*200), s.D) 35 | } 36 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/devices.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 4 | 5 | func (store *Store) GetDevices() *devices.List { 6 | store.RLock() 7 | defer store.RUnlock() 8 | return store.Devices 9 | } 10 | 11 | func (store *Store) AddOrUpdateDevice(dev *devices.Device) { 12 | if dev == nil { 13 | return 14 | } 15 | 16 | oldDev := store.Devices.Get(dev.ID) 17 | if oldDev != nil && oldDev.Equal(dev) { 18 | return 19 | } 20 | 21 | store.Devices.Add(dev) 22 | node := store.GetNode(dev.ID.Node) 23 | 24 | alias := node.Alias(dev.ID) 25 | if alias != dev.Alias { 26 | dev.Lock() 27 | dev.Alias = alias 28 | dev.Unlock() 29 | } 30 | 31 | store.Logic.UpdateDevice(dev) 32 | store.runCallbacks("devices") 33 | } 34 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/main.go: -------------------------------------------------------------------------------- 1 | //go:generate bash -c "go get -u github.com/rakyll/statik && cd web && rm -rf dist && npm run build && cd .. && statik -src ./web/dist -f" 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models" 8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/servermain" 9 | 10 | // Statik for the webserver gui. 11 | _ "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/statik" 12 | "github.com/stampzilla/stampzilla-go/v2/pkg/build" 13 | ) 14 | 15 | func main() { 16 | config := &models.Config{} 17 | config.MustLoad() 18 | 19 | if config.Version { 20 | fmt.Println(build.String()) 21 | return 22 | } 23 | 24 | server := servermain.New(config) 25 | server.Init() 26 | server.Run() 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | addons: 4 | apt: 5 | packages: 6 | - libasound2-dev 7 | 8 | go: 9 | - 1.20 10 | install: 11 | - go get -d -t -v ./... 12 | 13 | script: 14 | - make test 15 | - make cover 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | 19 | before_deploy: 20 | - go run cmd/build/build.go 21 | - cd dist && sha512sum * > checksum 22 | 23 | deploy: 24 | skip_cleanup: true 25 | provider: releases 26 | api_key: 27 | secure: bbn0U42cMuDYAQGCMaPXbkTOygk3Sr/P8u998Y13RVFEe/rcM4UQ9WjvhOMFlMLS3gN6Gqu/Qm3cT5z4AawY0JPf1PnbGprTMCZEzrHPmd203cAMRmcpzTYBEkioRRTgOS5syVxxY8ZNKCW4+/QAmngai3uU/CL/4aPL5T6SQSk= 28 | file_glob: true 29 | file: "*" 30 | on: 31 | tags: true 32 | repo: stampzilla/stampzilla-go 33 | condition: $TRAVIS_GO_VERSION =~ ^1\.20 34 | -------------------------------------------------------------------------------- /nodes/stampzilla-linux/health.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | sigar "github.com/cloudfoundry/gosigar" 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 8 | ) 9 | 10 | func monitorHealth() { 11 | dev := &devices.Device{ 12 | Name: "Health", 13 | ID: devices.ID{ID: "health"}, 14 | Online: true, 15 | State: devices.State{}, 16 | } 17 | n.AddOrUpdate(dev) 18 | 19 | for { 20 | uptime := sigar.Uptime{} 21 | uptime.Get() 22 | avg := sigar.LoadAverage{} 23 | avg.Get() 24 | 25 | newState := make(devices.State) 26 | newState["uptime"] = uptime.Format() 27 | newState["load_1"] = avg.One 28 | newState["load_5"] = avg.Five 29 | newState["load_15"] = avg.Fifteen 30 | n.UpdateState(dev.ID.ID, newState) 31 | 32 | <-time.After(time.Second * 1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/devices.md: -------------------------------------------------------------------------------- 1 | # Devices 2 | 3 | A device is a single unit which is controllable and/or reports some data. For example sensor, light etc. 4 | 5 | ### Device types 6 | 7 | type | Description 8 | --- | --- 9 | light | Can switch on or off 10 | sensor | can report any sensor data for example temperature 11 | button | Is a momentary button which can be pressed 12 | 13 | 14 | ### Traits 15 | 16 | A device can have one or multiple traits. This was inspired by the google actions API. 17 | 18 | ##### OnOff 19 | 20 | Required states 21 | 22 | state | type 23 | --- | --- 24 | on | bool 25 | 26 | ##### Brightness 27 | 28 | Required states 29 | 30 | state | type 31 | --- | --- 32 | brightness | float 0-1 33 | 34 | ##### ColorSetting 35 | 36 | Required states 37 | 38 | state | type 39 | --- | --- 40 | temperature | int 2000-6500 kelvin 41 | 42 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/nodes.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'nodes', 6 | ['UPDATE'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | list: Map(), 11 | }); 12 | 13 | // Actions 14 | export function update(connections) { 15 | return { type: c.UPDATE, connections }; 16 | } 17 | 18 | // Subscribe to channels and register the action for the packages 19 | export function subscribe(dispatch) { 20 | return { 21 | nodes: nodes => dispatch(update(nodes)), 22 | }; 23 | } 24 | 25 | // Reducer 26 | export default function reducer(state = defaultState, action) { 27 | switch (action.type) { 28 | case c.UPDATE: { 29 | return state 30 | .set('list', fromJS(action.connections)); 31 | } 32 | default: return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/devices.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'devices', 6 | ['UPDATE'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | list: Map(), 11 | }); 12 | 13 | // Actions 14 | export function update(connections) { 15 | return { type: c.UPDATE, connections }; 16 | } 17 | 18 | // Subscribe to channels and register the action for the packages 19 | export function subscribe(dispatch) { 20 | return { 21 | devices: devices => dispatch(update(devices)), 22 | }; 23 | } 24 | 25 | // Reducer 26 | export default function reducer(state = defaultState, action) { 27 | switch (action.type) { 28 | case c.UPDATE: { 29 | return state 30 | .set('list', fromJS(action.connections)); 31 | } 32 | default: return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/ducks/devices.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'devices', 6 | ['UPDATE'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | list: Map(), 11 | }); 12 | 13 | // Actions 14 | export function update(connections) { 15 | return { type: c.UPDATE, connections }; 16 | } 17 | 18 | // Subscribe to channels and register the action for the packages 19 | export function subscribe(dispatch) { 20 | return { 21 | devices: devices => dispatch(update(devices)), 22 | }; 23 | } 24 | 25 | // Reducer 26 | export default function reducer(state = defaultState, action) { 27 | switch (action.type) { 28 | case c.UPDATE: { 29 | return state 30 | .set('list', fromJS(action.connections)); 31 | } 32 | default: return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/requests.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'requests', 6 | ['UPDATE'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | list: Map(), 11 | }); 12 | 13 | // Actions 14 | export function update(connections) { 15 | return { type: c.UPDATE, connections }; 16 | } 17 | 18 | // Subscribe to channels and register the action for the packages 19 | export function subscribe(dispatch) { 20 | return { 21 | requests: requests => dispatch(update(requests)), 22 | }; 23 | } 24 | 25 | // Reducer 26 | export default function reducer(state = defaultState, action) { 27 | switch (action.type) { 28 | case c.UPDATE: { 29 | return state 30 | .set('list', fromJS(action.connections)); 31 | } 32 | default: return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/connections.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'connections', 6 | ['UPDATE'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | list: Map(), 11 | }); 12 | 13 | // Actions 14 | export function update(connections) { 15 | return { type: c.UPDATE, connections }; 16 | } 17 | 18 | // Subscribe to channels and register the action for the packages 19 | export function subscribe(dispatch) { 20 | return { 21 | connections: connections => dispatch(update(connections)), 22 | }; 23 | } 24 | 25 | // Reducer 26 | export default function reducer(state = defaultState, action) { 27 | switch (action.type) { 28 | case c.UPDATE: { 29 | return state 30 | .set('list', fromJS(action.connections)); 31 | } 32 | default: return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/certificates.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'certificates', 6 | ['UPDATE'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | list: Map(), 11 | }); 12 | 13 | // Actions 14 | export function update(connections) { 15 | return { type: c.UPDATE, connections }; 16 | } 17 | 18 | // Subscribe to channels and register the action for the packages 19 | export function subscribe(dispatch) { 20 | return { 21 | certificates: certificates => dispatch(update(certificates)), 22 | }; 23 | } 24 | 25 | // Reducer 26 | export default function reducer(state = defaultState, action) { 27 | switch (action.type) { 28 | case c.UPDATE: { 29 | return state 30 | .set('list', fromJS(action.connections)); 31 | } 32 | default: return state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/certificates.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "time" 4 | 5 | type Certificate struct { 6 | Serial string `json:"serial"` 7 | Subject RequestSubject `json:"subject"` 8 | CommonName string `json:"commonName"` 9 | IsCA bool `json:"isCA"` 10 | Usage []string `json:"usage"` 11 | Revoked bool `json:"revoked"` 12 | Issued time.Time `json:"issued"` 13 | Expires time.Time `json:"expires"` 14 | 15 | Fingerprints map[string]string `json:"fingerprints"` 16 | } 17 | 18 | func (store *Store) GetCertificates() []Certificate { 19 | store.RLock() 20 | defer store.RUnlock() 21 | return store.Certificates 22 | } 23 | 24 | func (store *Store) UpdateCertificates(certs []Certificate) { 25 | store.Lock() 26 | store.Certificates = certs 27 | store.Unlock() 28 | 29 | store.runCallbacks("certificates") 30 | } 31 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "react", 6 | "html" 7 | ], 8 | 9 | "env": { 10 | "browser": true 11 | }, 12 | 13 | "extends": "airbnb", 14 | 15 | "rules": { 16 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 17 | "react/require-default-props": 0, 18 | "jsx-a11y/media-has-caption": 0, 19 | "function-paren-newline": ["error", "consistent"], 20 | "jsx-a11y/anchor-is-valid": 0, 21 | "jsx-a11y/click-events-have-key-events": 0, 22 | "object-curly-newline": ["error", { "minProperties": 4, "consistent": true }], 23 | "jsx-a11y/label-has-for": 0, 24 | "jsx-a11y/no-autofocus": 0, 25 | "camelcase": 0, 26 | }, 27 | 28 | "settings": { 29 | "import/resolver": { 30 | "node": { 31 | "paths": ["src"] 32 | } 33 | }, 34 | "html/report-bad-indent": "warn" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/README.md: -------------------------------------------------------------------------------- 1 |

React, React Router, Redux and Redux Thunk

2 | 3 | * Tutorial: [Getting started with create-react-app, Redux, React Router & Redux Thunk](https://medium.com/@notrab/getting-started-with-create-react-app-redux-react-router-redux-thunk-d6a19259f71f) 4 | * [Demo](https://create-react-app-redux.now.sh) 🙌 5 | 6 | ## Installation 7 | 8 | ```bash 9 | git clone https://github.com/notrab/create-react-app-redux.git 10 | cd create-react-app-redux 11 | yarn 12 | ``` 13 | 14 | ## Get started 15 | 16 | ```bash 17 | yarn start 18 | ``` 19 | 20 | This boilerplate is built using [create-react-app](https://github.com/facebook/create-react-app) so you will want to read the User Guide for more goodies. 21 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | 3 | import app from './app'; 4 | import certificates from './certificates'; 5 | import connection from './connection'; 6 | import connections from './connections'; 7 | import destinations from './destinations'; 8 | import devices from './devices'; 9 | import nodes from './nodes'; 10 | import persons from './persons'; 11 | import requests from './requests'; 12 | import rules from './rules'; 13 | import savedstates from './savedstates'; 14 | import schedules from './schedules'; 15 | import senders from './senders'; 16 | import server from './server'; 17 | 18 | const rootReducer = combineReducers({ 19 | app, 20 | certificates, 21 | connection, 22 | connections, 23 | destinations, 24 | devices, 25 | nodes, 26 | persons, 27 | requests, 28 | rules, 29 | savedstates, 30 | schedules, 31 | senders, 32 | server, 33 | }); 34 | 35 | export default rootReducer; 36 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/CustomCheckbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CustomCheckbox = (props) => { 4 | const { 5 | id, 6 | value, 7 | required, 8 | disabled, 9 | readonly, 10 | label, 11 | autofocus, 12 | onChange, 13 | } = props; 14 | return ( 15 |
16 | onChange(event.target.checked)} 25 | /> 26 | 29 |
30 | ); 31 | }; 32 | 33 | export default CustomCheckbox; 34 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import { routerMiddleware } from 'connected-react-router/immutable' 3 | import thunk from 'redux-thunk' 4 | import createHistory from 'history/createBrowserHistory' 5 | import rootReducer from './ducks' 6 | import { Map } from 'immutable' 7 | 8 | export const history = createHistory() 9 | 10 | const initialState = Map({}) 11 | const enhancers = [] 12 | const middleware = [thunk, routerMiddleware(history)] 13 | 14 | if (process.env.NODE_ENV === 'development') { 15 | const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION__ 16 | 17 | if (typeof devToolsExtension === 'function') { 18 | enhancers.push(devToolsExtension()) 19 | } 20 | } 21 | 22 | const composedEnhancers = compose( 23 | applyMiddleware(...middleware), 24 | ...enhancers 25 | ) 26 | 27 | export default createStore( 28 | rootReducer(history), 29 | initialState, 30 | composedEnhancers 31 | ) 32 | -------------------------------------------------------------------------------- /nodes/stampzilla-modbus/modbus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/goburrow/modbus" 7 | ) 8 | 9 | type Modbus struct { 10 | client modbus.Client 11 | handler *modbus.RTUClientHandler 12 | } 13 | 14 | func (m *Modbus) Connect() error { 15 | // Modbus RTU/ASCII 16 | handler := modbus.NewRTUClientHandler("/dev/ttyUSB0") 17 | handler.BaudRate = 9600 18 | handler.DataBits = 8 19 | handler.Parity = "N" 20 | handler.StopBits = 2 21 | handler.SlaveId = 1 22 | handler.Timeout = 5 * time.Second 23 | // handler.Logger = log.New(os.Stdout, "test: ", log.LstdFlags) 24 | m.handler = handler 25 | if err := handler.Connect(); err != nil { 26 | return err 27 | } 28 | 29 | m.client = modbus.NewClient(handler) 30 | return nil 31 | } 32 | 33 | func (m *Modbus) Close() error { 34 | return m.handler.Close() 35 | } 36 | 37 | func (m *Modbus) ReadInputRegister(address uint16) ([]byte, error) { 38 | return m.client.ReadInputRegisters(address-1, 1) 39 | } 40 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/server.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 8 | ) 9 | 10 | func (s *Store) AddOrUpdateServer(area, item string, state devices.State) { 11 | s.Lock() 12 | if s.Server[area] == nil { 13 | s.Server[area] = make(map[string]devices.State) 14 | } 15 | if s.Server[area][item] == nil { 16 | s.Server[area][item] = make(devices.State) 17 | } 18 | 19 | if diff := s.Server[area][item].Diff(state); len(diff) != 0 { 20 | s.Server[area][item].MergeWith(diff) 21 | s.Unlock() 22 | s.runCallbacks("server") 23 | return 24 | } 25 | s.Unlock() 26 | } 27 | 28 | func (store *Store) GetServerStateAsJson() json.RawMessage { 29 | store.RLock() 30 | b, err := json.Marshal(store.Server) 31 | store.RUnlock() 32 | 33 | if err != nil { 34 | logrus.Errorf("Failed to marshal server state: %s", err.Error()) 35 | } 36 | return b 37 | } 38 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/SocketModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import FormModal from './FormModal'; 4 | 5 | const schema = { 6 | type: 'object', 7 | required: ['hostname', 'port'], 8 | properties: { 9 | hostname: { type: 'string', title: 'Hostname', default: location.hostname }, 10 | port: { type: 'string', title: 'Port', default: location.port }, 11 | }, 12 | }; 13 | 14 | class SocketModal extends Component { 15 | onChange = () => ({ formData }) => { 16 | this.props.onChange(formData); 17 | this.props.onClose(); 18 | }; 19 | 20 | render = () => { 21 | const { visible, onClose } = this.props; 22 | 23 | return ( 24 | 32 | ); 33 | }; 34 | } 35 | 36 | export default SocketModal; 37 | -------------------------------------------------------------------------------- /coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | readonly GOPATH="${GOPATH%%:*}" 6 | 7 | main() { 8 | _cd_into_top_level 9 | _generate_coverage_files 10 | _combine_coverage_reports 11 | } 12 | 13 | _cd_into_top_level() { 14 | cd "$(git rev-parse --show-toplevel)" 15 | #cd nodes/stampzilla-server 16 | } 17 | 18 | _generate_coverage_files() { 19 | for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -not -path '*/stampzilla-telldus-events' -not -path '*/stampzilla-hidcommander' -type d); do 20 | if ls $dir/*.go &>/dev/null ; then 21 | go test -covermode=atomic -coverprofile=$dir/profile.coverprofile $dir || fail=1 22 | fi 23 | done 24 | } 25 | 26 | _install_gover() { 27 | if [[ ! -x "${GOPATH}/bin/gover" ]] ; then 28 | go install github.com/modocache/gover@latest 29 | fi 30 | } 31 | 32 | _combine_coverage_reports() { 33 | _install_gover 34 | ${GOPATH}/bin/gover 35 | mv gover.coverprofile coverage.txt 36 | } 37 | 38 | main "$@" 39 | -------------------------------------------------------------------------------- /nodes/stampzilla-enocean/handlersinit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jonaz/goenocean" 5 | ) 6 | 7 | var handlers *eepHandlers 8 | 9 | type eepHandlers struct { 10 | handlers map[string]Handler 11 | } 12 | 13 | type Handler interface { 14 | On(*Device) 15 | Off(*Device) 16 | Toggle(*Device) 17 | Learn(*Device) 18 | Dim(int, *Device) 19 | Process(*Device, goenocean.Telegram) 20 | } 21 | 22 | func (h *eepHandlers) getHandler(t string) Handler { 23 | if handler, ok := h.handlers[t]; ok { 24 | return handler 25 | } 26 | return nil 27 | } 28 | 29 | func init() { 30 | handlers = &eepHandlers{make(map[string]Handler)} 31 | handlers.handlers["a53808"] = &handlerEepa53808{} 32 | handlers.handlers["a53808eltako"] = &handlerEepa53808eltako{} 33 | handlers.handlers["d20109"] = &handlerEepd20109{} 34 | handlers.handlers["a51201"] = &handlerEepa51201{} 35 | handlers.handlers["f60201"] = &handlerEepf60201{} 36 | handlers.handlers["f60201eltako"] = &handlerEepf60201eltako{} 37 | } 38 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/Wrapper.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router } from 'react-router-dom'; 2 | import { connect } from 'react-redux'; 3 | import React from 'react'; 4 | 5 | import App from './App'; 6 | import Landing from './Landing'; 7 | import Routes from '../routes'; 8 | import Websocket from './Websocket'; 9 | 10 | const Wrapper = (props) => { 11 | const { server, connection } = props; 12 | 13 | const secure = (window.location.protocol.match(/^https/) || server.get('secure')) 14 | && connection !== 4001; 15 | 16 | return ( 17 | 18 | 19 | {!secure && } 20 | {secure && ( 21 | 22 | 23 | 24 | 25 | 26 | )} 27 | 28 | ); 29 | }; 30 | 31 | const mapToProps = state => ({ 32 | server: state.get('server'), 33 | connection: state.getIn(['connection', 'code']), 34 | }); 35 | 36 | export default connect(mapToProps)(Wrapper); 37 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | const Card = (props) => { 5 | const { 6 | title, children, toolbar, bodyClassName, className, 7 | } = props; 8 | return ( 9 |
10 |
11 |

{title}

12 |
13 | {toolbar 14 | && toolbar.map((tool) => ( 15 | 23 | ))} 24 |
25 |
26 |
{children}
27 |
28 | ); 29 | }; 30 | 31 | export default Card; 32 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/logic/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "e8092b86-1261-44cd-ab64-38121df58a79": { 3 | "name": "All off", 4 | "enabled": true, 5 | "active": false, 6 | "uuid": "e8092b86-1261-44cd-ab64-38121df58a79", 7 | "expression": "devices['asdf.123'].on == true && devices['asdf.123'].temperature > 20.0", 8 | "conditions": { 9 | "1fd25327-f43c-4a00-aa67-3969dfed06b5": true 10 | }, 11 | "for": "5m", 12 | "actions": [ 13 | "1m", 14 | "c7d352bb-23f4-468c-b476-f76599c09a0d" 15 | ] 16 | }, 17 | "1fd25327-f43c-4a00-aa67-3969dfed06b5": { 18 | "name": "chromecast p\u00e5", 19 | "enabled": true, 20 | "active": false, 21 | "uuid": "1fd25327-f43c-4a00-aa67-3969dfed06b5", 22 | "expression": "devices['asdf.123'].on == true ", 23 | "conditions": {}, 24 | "for": "5m", 25 | "actions": [ 26 | "1m", 27 | "c7d352bb-23f4-468c-b476-f76599c09a0d" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/logic.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/logic" 5 | ) 6 | 7 | func (store *Store) GetRules() logic.Rules { 8 | store.Logic.RLock() 9 | defer store.Logic.RUnlock() 10 | return store.Logic.Rules 11 | } 12 | 13 | func (store *Store) AddOrUpdateRules(rules logic.Rules) { 14 | store.Logic.SetRules(rules) 15 | store.Logic.Save() 16 | store.runCallbacks("rules") 17 | } 18 | 19 | func (store *Store) GetSavedStates() logic.SavedStates { 20 | return store.Logic.StateStore.All() 21 | } 22 | 23 | func (store *Store) AddOrUpdateSavedStates(s logic.SavedStates) { 24 | store.SavedState.SetState(s) 25 | store.SavedState.Save() 26 | store.runCallbacks("savedstates") 27 | } 28 | 29 | func (store *Store) GetScheduledTasks() logic.Tasks { 30 | return store.Scheduler.Tasks() 31 | } 32 | 33 | func (store *Store) AddOrUpdateScheduledTasks(tasks logic.Tasks) { 34 | store.Scheduler.SetTasks(tasks) 35 | store.Scheduler.Save() 36 | store.runCallbacks("schedules") 37 | } 38 | -------------------------------------------------------------------------------- /pkg/installer/pidfile.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type PidFile string 11 | 12 | func (f *PidFile) String() string { 13 | return string(*f) 14 | } 15 | 16 | // Read the pidfile. 17 | func (f *PidFile) Read() int { 18 | data, err := ioutil.ReadFile(string(*f)) 19 | if err != nil { 20 | return 0 21 | } 22 | pidString := strings.Trim(string(data), "\n") 23 | pid, err := strconv.ParseInt(string(pidString), 0, 32) 24 | if err != nil { 25 | return 0 26 | } 27 | return int(pid) 28 | } 29 | 30 | // Write the pidfile. 31 | func (f *PidFile) write(data int) error { 32 | err := ioutil.WriteFile(string(*f), []byte(strconv.Itoa(data)), 0660) 33 | if err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | // Delete the pidfile. 40 | func (f *PidFile) delete() bool { 41 | _, err := os.Stat(string(*f)) 42 | if err != nil { 43 | return true 44 | } 45 | err = os.Remove(string(*f)) 46 | if err == nil { 47 | return true 48 | } 49 | return false 50 | } 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test cover cover-html cover-test 2 | 3 | test: 4 | go test `go list ./... | grep -v /vendor/ | grep -v stampzilla-telldus` 5 | 6 | # todo: use this when golang issue 23910 is resolved 7 | # go test -v -coverpkg=./... -coverprofile=all `go list ./... | grep -v /vendor/ ` 8 | cover: 9 | @echo Running coverage 10 | go install github.com/wadey/gocovmerge@latest 11 | $(eval PKGS := $(shell go list ./... | grep -v /vendor/ )) 12 | $(eval PKGS_DELIM := $(shell echo $(PKGS) | sed -e 's/ /,/g')) 13 | go list -f '{{if or (len .TestGoFiles) (len .XTestGoFiles)}}go test -test.v -test.timeout=120s -covermode=atomic -coverprofile={{.Name}}_{{len .Imports}}_{{len .Deps}}.coverprofile -coverpkg $(PKGS_DELIM) {{.ImportPath}}{{end}}' $(PKGS) | xargs -I {} bash -c {} 14 | gocovmerge `ls *.coverprofile` > coverage.txt 15 | rm *.coverprofile 16 | cover-normal: 17 | bash coverage 18 | 19 | cover-html: cover 20 | go tool cover -html coverage.txt 21 | 22 | build-ui: 23 | cd nodes/stampzilla-server/public && gulp 24 | cd nodes/stampzilla-server && go generate 25 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/connections.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models" 4 | 5 | func (store *Store) GetConnections() Connections { 6 | store.RLock() 7 | conns := make(Connections) 8 | for k, v := range store.Connections { 9 | conns[k] = v 10 | } 11 | store.RUnlock() 12 | return conns 13 | } 14 | 15 | func (store *Store) Connection(id string) *models.Connection { 16 | store.RLock() 17 | defer store.RUnlock() 18 | if conn, ok := store.Connections[id]; ok { 19 | return conn 20 | } 21 | return nil 22 | } 23 | 24 | func (store *Store) AddOrUpdateConnection(id string, c *models.Connection) { 25 | store.Lock() 26 | store.Connections[id] = c 27 | store.Unlock() 28 | 29 | store.runCallbacks("connections") 30 | } 31 | 32 | func (store *Store) ConnectionChanged() { 33 | store.runCallbacks("connections") 34 | } 35 | 36 | func (store *Store) RemoveConnection(id string) { 37 | store.Lock() 38 | delete(store.Connections, id) 39 | store.Unlock() 40 | store.runCallbacks("connections") 41 | } 42 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "react", 6 | "html" 7 | ], 8 | 9 | "env": { 10 | "browser": true 11 | }, 12 | 13 | "extends": "airbnb", 14 | 15 | "rules": { 16 | "camelcase": 0, 17 | "function-paren-newline": ["error", "consistent"], 18 | "jsx-a11y/anchor-is-valid": 0, 19 | "jsx-a11y/click-events-have-key-events": 0, 20 | "jsx-a11y/label-has-for": 0, 21 | "jsx-a11y/media-has-caption": 0, 22 | "jsx-a11y/no-autofocus": 0, 23 | "max-len": 0, 24 | "object-curly-newline": ["error", { "minProperties": 4, "consistent": true }], 25 | "react/destructuring-assignment": 0, 26 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 27 | "react/prop-types": 0, 28 | "react/require-default-props": 0, 29 | "jsx-a11y/label-has-associated-control": 0, 30 | }, 31 | 32 | "settings": { 33 | "import/resolver": { 34 | "node": { 35 | "paths": ["src"] 36 | } 37 | }, 38 | "html/report-bad-indent": "warn" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stampzilla 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/FormModal.scss: -------------------------------------------------------------------------------- 1 | .formModal { 2 | table { 3 | th { 4 | border-top: 0 !important; 5 | padding-top: 0 !important; 6 | } 7 | 8 | .reason { 9 | border-top: 0; 10 | margin-top: 0; 11 | padding-left: 30px; 12 | background: #eee; 13 | box-shadow: inset 0px 5px 15px -5px rgba(0,0,0,0.75); 14 | } 15 | } 16 | :global(.field-description) { 17 | color: #999; 18 | } 19 | :global(.modal-body) { 20 | dl:last-of-type { 21 | margin-bottom: 0 !important; 22 | 23 | dd:last-of-type { 24 | margin-bottom:0 !important; 25 | 26 | p:last-of-type { 27 | margin-bottom:0 !important; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | .footerContainer { 35 | .buttons { 36 | display: flex; 37 | align-items: center; 38 | justify-content: flex-end; 39 | 40 | & > button, 41 | & > :global(.btn-group) { 42 | margin-left: 1rem; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jamie Barton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/installer/binary/releases.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/github" 7 | ) 8 | 9 | func GetReleases() []*github.RepositoryRelease { 10 | client := github.NewClient(nil) 11 | ctx := context.Background() 12 | releases, _, err := client.Repositories.ListReleases(ctx, "stampzilla", "stampzilla-go", &github.ListOptions{}) 13 | if err != nil { 14 | return nil 15 | } 16 | 17 | return releases 18 | 19 | ////commits, _, err := client.Repositories.ListCommits(ctx, "stampzilla", "stampzilla-go" 20 | 21 | // url := "https://api.github.com/repos/stampzilla/stampzilla-go/releases" 22 | 23 | // req, err := http.NewRequest("GET", url, nil) 24 | // if err != nil { 25 | // log.Fatal("NewRequest: ", err) 26 | // return []Release{} 27 | //} 28 | 29 | // client := &http.Client{} 30 | 31 | // resp, err := client.Do(req) 32 | // if err != nil { 33 | // log.Fatal("Do: ", err) 34 | // return []Release{} 35 | //} 36 | 37 | // defer resp.Body.Close() 38 | 39 | // var releases []Release 40 | 41 | // if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { 42 | // log.Println(err) 43 | //} 44 | 45 | // return releases 46 | } 47 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/pushover/pushover.go: -------------------------------------------------------------------------------- 1 | package pushover 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | pover "github.com/gregdel/pushover" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var ErrNotImplemented = fmt.Errorf("not implemented") 12 | 13 | type PushOver struct { 14 | Token string `json:"token"` 15 | } 16 | 17 | func New(parameters json.RawMessage) *PushOver { 18 | pb := &PushOver{} 19 | 20 | err := json.Unmarshal(parameters, pb) 21 | if err != nil { 22 | logrus.Error(err) 23 | } 24 | return pb 25 | } 26 | 27 | func (po *PushOver) Release(dest []string, body string) error { 28 | return ErrNotImplemented 29 | } 30 | 31 | func (po *PushOver) Trigger(dest []string, body string) error { 32 | app := pover.New(po.Token) 33 | var err error 34 | 35 | for _, userKey := range dest { 36 | recipient := pover.NewRecipient(userKey) 37 | message := pover.NewMessage(body) 38 | message.Title = "Stampzilla" 39 | _, err = app.SendMessage(message, recipient) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | return err 45 | } 46 | 47 | func (po *PushOver) Destinations() (map[string]string, error) { 48 | return nil, ErrNotImplemented 49 | } 50 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/server_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestAddOrUpdateServer(t *testing.T) { 12 | store := &Store{ 13 | Server: make(map[string]map[string]devices.State), 14 | } 15 | 16 | i := int64(0) 17 | store.OnUpdate(func(str string, s *Store) error { 18 | atomic.AddInt64(&i, 1) 19 | assert.Equal(t, "server", str) 20 | return nil 21 | }) 22 | 23 | store.AddOrUpdateServer("area", "item", devices.State{ 24 | "state1": true, 25 | }) 26 | store.AddOrUpdateServer("area", "item", devices.State{ 27 | "state1": true, 28 | }) 29 | store.AddOrUpdateServer("area", "item", devices.State{ 30 | "state2": true, 31 | }) 32 | 33 | assert.Equal(t, int64(2), i) 34 | } 35 | 36 | func TestGetServerStateAsJson(t *testing.T) { 37 | store := &Store{ 38 | Server: make(map[string]map[string]devices.State), 39 | } 40 | store.AddOrUpdateServer("area", "item", devices.State{ 41 | "state1": true, 42 | }) 43 | data := store.GetServerStateAsJson() 44 | assert.Contains(t, string(data), "state1") 45 | } 46 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/routes/home/Clock.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import moment from 'moment' 3 | 4 | class Clock extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | this.clock = React.createRef() 8 | this.seconds = React.createRef() 9 | this.date = React.createRef() 10 | } 11 | 12 | componentDidMount() { 13 | const updateClock = () => { 14 | this.clock.current.innerHTML = moment().format('HH:mm') 15 | this.seconds.current.innerHTML = moment().format('ss') 16 | this.date.current.innerHTML = moment().format('dddd - D MMMM') 17 | } 18 | updateClock() 19 | this.clockInterval = setInterval(updateClock, 1000) 20 | } 21 | 22 | componentWillUnmount() { 23 | clearTimeout(this.clockInterval) 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 |
30 |
00:00
31 |
32 | 00 33 |
34 |
35 |
36 | - 37 |
38 |
39 | ) 40 | } 41 | } 42 | 43 | export default Clock 44 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/ducks/connection.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | 4 | const c = defineAction( 5 | 'connection', 6 | ['CONNECTED', 'DISCONNECTED', 'ERROR'], 7 | ); 8 | 9 | const defaultState = Map({ 10 | connected: null, 11 | error: null, 12 | }); 13 | 14 | // Actions 15 | export function connected() { 16 | return { type: c.CONNECTED }; 17 | } 18 | 19 | export function disconnected() { 20 | return (dispatch, getState) => { 21 | if (getState().getIn(['connection', 'connected']) !== false) { 22 | dispatch({ type: c.DISCONNECTED }); 23 | } 24 | }; 25 | } 26 | 27 | export function error(err) { 28 | return { type: c.ERROR, error: err }; 29 | } 30 | 31 | // Reducer 32 | export default function reducer(state = defaultState, action) { 33 | switch (action.type) { 34 | case c.CONNECTED: { 35 | return state 36 | .set('error', null) 37 | .set('connected', true); 38 | } 39 | case c.DISCONNECTED: { 40 | return state 41 | .set('connected', false); 42 | } 43 | case c.ERROR: { 44 | return state 45 | .set('error', action.error); 46 | } 47 | default: return state; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/routes/home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import Clock from './Clock' 5 | import Forecast from './Forecast' 6 | import DeviceList from './DeviceList' 7 | 8 | class Home extends React.PureComponent { 9 | render() { 10 | const { widgets } = this.props 11 | return ( 12 |
13 | {widgets && 14 | widgets.map((widget, index) => { 15 | switch (widget.get('type')) { 16 | case 'clock': 17 | return 18 | case 'forecast': 19 | return 20 | case 'devicelist': 21 | return ( 22 | 26 | ) 27 | default: 28 | return null 29 | } 30 | })} 31 |
32 | ) 33 | } 34 | } 35 | 36 | const mapStateToProps = state => ({ 37 | widgets: state.getIn(['config', 'widgets']) 38 | }) 39 | 40 | export default connect(mapStateToProps)(Home) 41 | -------------------------------------------------------------------------------- /nodes/stampzilla-chromecast/state.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type State struct { 8 | Chromecasts map[string]*Chromecast 9 | sync.RWMutex 10 | } 11 | 12 | func (s *State) GetByUUID(uuid string) *Chromecast { 13 | s.RLock() 14 | defer s.RUnlock() 15 | if val, ok := s.Chromecasts[uuid]; ok { 16 | return val 17 | } 18 | return nil 19 | } 20 | 21 | func (s *State) Add(c *Chromecast) { 22 | s.Lock() 23 | s.Chromecasts[c.Uuid()] = c 24 | s.Unlock() 25 | 26 | /* 27 | s.node.Chromecasts.Add(&devices.Device{ 28 | Type: "chromecast", 29 | Name: c.Name(), 30 | Id: c.Id, 31 | Online: true, 32 | StateMap: map[string]string{ 33 | "Playing": c.Id + ".Playing", 34 | "PrimaryApp": c.Id + ".PrimaryApp", 35 | "Title": c.Id + ".Media.Title", 36 | "SubTitle": c.Id + ".Media.SubTitle", 37 | "Thumb": c.Id + ".Media.Thumb", 38 | "Url": c.Id + ".Media.Url", 39 | "Duration": c.Id + ".Media.Duration", 40 | }, 41 | }) 42 | */ 43 | } 44 | 45 | // Remove removes a chromecast from the state. 46 | func (s *State) Remove(c *Chromecast) { 47 | _, ok := s.Chromecasts[c.Uuid()] 48 | if ok { 49 | s.Lock() 50 | delete(s.Chromecasts, c.Uuid()) 51 | s.Unlock() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | * [Devices](devices.md) 4 | * [Screenshots](screenshots.md) 5 | 6 | 7 | ## Architecture 8 | 9 | stampzilla-go is composed of alot of standalone services or nodes as we call them (nowdays they are commonly known as microservices). 10 | There is a cli which can be used to start, stop and show status of the running services. 11 | available official nodes can be found in the nodes directory. 12 | 13 | All nodes report their state to the server node which have schedule and rule engines. The server also have capabilities to log all state changes as metrics to influxdb which mean you can easilly draw temperature graphs or see how long a lamp has been on over time. 14 | 15 | 16 | ## Nodes 17 | 18 | Nodes can be configured in the web interface. The config is a json object and default config for each node can be found in the links below. 19 | Its up to the node if it requires config or not. For example telldus does not require any config but takes all config from the telldusd running on the same machine. 20 | 21 | * [deconz](../nodes/stampzilla-deconz/README.md) 22 | * [google-assistant](../nodes/stampzilla-google-assistant/README.md) 23 | * [server](../nodes/stampzilla-server/README.md) 24 | * [telldus](../nodes/stampzilla-telldus/README.md) 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /nodes/stampzilla-husdata-h60/model_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHeatPumpState(t *testing.T) { 11 | tests := []struct { 12 | in string 13 | out string 14 | }{ 15 | // 65529 - 65536 = -7 16 | {`{"0007":65529}`, `"Outdoor":-0.7`}, 17 | {`{"0007":-7}`, `"Outdoor":-0.7`}, 18 | {`{"0007":-71}`, `"Outdoor":-7.1`}, 19 | {`{"0007":123}`, `"Outdoor":12.3`}, 20 | {`{"1A01":1}`, `"Compressor":true`}, 21 | {`{"1A01":0}`, `"Compressor":false`}, 22 | {`{"5C52":844329}`, `"SuppliedEnergyHeating":8443.29`}, 23 | {`{"5C53":463482}`, `"SuppliedEnergyHotwater":4634.82`}, 24 | {`{"5C55":4758500}`, `"CompressorConsumptionHeating":4758.5`}, 25 | {`{"5C56":2736630}`, `"CompressorConsumptionHotwater":2736.63`}, 26 | {`{"5C58":3608}`, `"AuxConsumptionHeating":3.608`}, 27 | {`{"5C59":250314}`, `"AuxConsumptionHotwater":250.314`}, 28 | } 29 | 30 | for _, tt := range tests { 31 | data := tt 32 | t.Run(data.in, func(t *testing.T) { 33 | hp := &HeatPump{} 34 | err := json.Unmarshal([]byte(data.in), hp) 35 | assert.NoError(t, err) 36 | out, err := json.Marshal(hp.State()) 37 | assert.NoError(t, err) 38 | assert.Contains(t, string(out), data.out) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | componentDidCatch(error, info) { 10 | this.setState({ 11 | hasError: true, 12 | error, 13 | info, 14 | }); 15 | } 16 | 17 | render() { 18 | const { hasError, error, info } = this.state; 19 | 20 | if (hasError) { 21 | return ( 22 |
23 |

Something went wrong.

24 |
{error.toString()}
25 | 26 | Stack trace 27 |
{error.stack.replace(/webpack:\/\/\/.\//g, '')}
28 | {info && ( 29 | 30 | Component trace 31 |
{info.componentStack.replace(/^\s+|\s+$/g, '')}
32 |
33 | )} 34 |
35 | ); 36 | } 37 | 38 | return this.props.children; 39 | } 40 | } 41 | 42 | export default ErrorBoundary; 43 | 44 | export const withBoudary = WrappedComponent => props => ( 45 | 46 | 47 | 48 | ); 49 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/helpers/isprivateip.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | ) 8 | 9 | var privateIPBlocks []*net.IPNet 10 | 11 | // Credits to https://stackoverflow.com/questions/41240761/check-if-ip-address-is-in-private-network-space/50825191#50825191 12 | func init() { 13 | for _, cidr := range []string{ 14 | "127.0.0.0/8", // IPv4 loopback 15 | "10.0.0.0/8", // RFC1918 16 | "172.16.0.0/12", // RFC1918 17 | "192.168.0.0/16", // RFC1918 18 | "169.254.0.0/16", // RFC3927 link-local 19 | "::1/128", // IPv6 loopback 20 | "fe80::/10", // IPv6 link-local 21 | "fc00::/7", // IPv6 unique local addr 22 | } { 23 | _, block, err := net.ParseCIDR(cidr) 24 | if err != nil { 25 | panic(fmt.Errorf("parse error on %q: %v", cidr, err)) 26 | } 27 | privateIPBlocks = append(privateIPBlocks, block) 28 | } 29 | } 30 | 31 | func IsPrivateIP(ipStr string) bool { 32 | u, err := url.Parse("http://" + ipStr) 33 | if err != nil { 34 | return false 35 | } 36 | 37 | ip := net.ParseIP(u.Hostname()) 38 | 39 | if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { 40 | return true 41 | } 42 | 43 | for _, block := range privateIPBlocks { 44 | if block.Contains(ip) { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/node.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 8 | "github.com/stampzilla/stampzilla-go/v2/pkg/build" 9 | ) 10 | 11 | type Node struct { 12 | UUID string `json:"uuid,omitempty"` 13 | Connected_ bool `json:"connected,omitempty"` 14 | Build build.Data `json:"build,omitempty"` 15 | Type string `json:"type,omitempty"` 16 | Name string `json:"name,omitempty"` 17 | // Devices Devices `json:"devices,omitempty"` 18 | Config json.RawMessage `json:"config,omitempty"` 19 | Aliases map[devices.ID]string `json:"aliases,omitempty"` 20 | sync.Mutex 21 | } 22 | 23 | func (n *Node) SetConnected(c bool) { 24 | n.Lock() 25 | n.Connected_ = c 26 | n.Unlock() 27 | } 28 | 29 | func (n *Node) Connected() bool { 30 | n.Lock() 31 | defer n.Unlock() 32 | return n.Connected_ 33 | } 34 | 35 | func (n *Node) SetAlias(id devices.ID, alias string) { 36 | n.Lock() 37 | if n.Aliases == nil { 38 | n.Aliases = make(map[devices.ID]string) 39 | } 40 | n.Aliases[id] = alias 41 | n.Unlock() 42 | } 43 | 44 | func (n *Node) Alias(id devices.ID) string { 45 | n.Lock() 46 | defer n.Unlock() 47 | if a, ok := n.Aliases[id]; ok { 48 | return a 49 | } 50 | return "" 51 | } 52 | -------------------------------------------------------------------------------- /nodes/stampzilla-mbus/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "192.168.13.42", 3 | "port": "10001", 4 | "devices": [ 5 | { 6 | "name": "Electrical Meter", 7 | "interval": "30s", 8 | "primaryAddress": 1, 9 | "Frames": { 10 | "0": [ 11 | { 12 | "id": 0, 13 | "name": "total" 14 | }, 15 | { 16 | "id": 2, 17 | "name": "current" 18 | } 19 | ] 20 | } 21 | }, 22 | { 23 | "name": "Coldwater", 24 | "interval": "10s", 25 | "primaryAddress": 2, 26 | "Frames": { 27 | "0": [ 28 | { 29 | "id": 6, 30 | "name": "total" 31 | } 32 | ] 33 | } 34 | }, 35 | { 36 | "name": "Hotwater", 37 | "interval": "10s", 38 | "primaryAddress": 3, 39 | "Frames": { 40 | "0": [ 41 | { 42 | "id": 6, 43 | "name": "total" 44 | } 45 | ] 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /nodes/stampzilla-1wire/onewire/1wire.go: -------------------------------------------------------------------------------- 1 | package onewire 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | var ErrSensorRead = errors.New("failed to read temperature from sensor") 12 | 13 | func SensorsWithTemperature() ([]string, error) { 14 | data, err := ioutil.ReadFile("/sys/bus/w1/devices/w1_bus_master1/w1_master_slaves") 15 | if err != nil { 16 | return nil, fmt.Errorf("error reading 1wire from sys filesystem: %w", err) 17 | } 18 | sensors := []string{} 19 | for _, s := range strings.Split(string(data), "\n") { 20 | s = strings.TrimSpace(s) 21 | if _, err := Temperature(s); err != nil { 22 | continue 23 | } 24 | sensors = append(sensors, s) 25 | } 26 | return sensors, nil 27 | } 28 | 29 | // Temperature get the temp of sensor with id. 30 | func Temperature(sensor string) (float64, error) { 31 | data, err := ioutil.ReadFile("/sys/bus/w1/devices/" + sensor + "/w1_slave") 32 | if err != nil { 33 | return 0.0, ErrSensorRead 34 | } 35 | 36 | raw := string(data) 37 | 38 | if !strings.Contains(raw, " YES") { 39 | return 0.0, ErrSensorRead 40 | } 41 | 42 | i := strings.LastIndex(raw, "t=") 43 | if i == -1 { 44 | return 0.0, ErrSensorRead 45 | } 46 | 47 | c, err := strconv.ParseFloat(raw[i+2:len(raw)-1], 64) 48 | if err != nil { 49 | return 0.0, ErrSensorRead 50 | } 51 | 52 | return c / 1000.0, nil 53 | } 54 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/email/email.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/smtp" 7 | ) 8 | 9 | type EmailSender struct { 10 | Server string `json:"server"` 11 | Port int `json:"port"` 12 | From string `json:"from"` 13 | Password string `json:"password"` 14 | send func(string, smtp.Auth, string, []string, []byte) error 15 | } 16 | 17 | func New(parameters json.RawMessage) *EmailSender { 18 | es := &EmailSender{send: smtp.SendMail} 19 | 20 | json.Unmarshal(parameters, es) 21 | 22 | return es 23 | } 24 | 25 | func (es *EmailSender) Trigger(dest []string, body string) error { 26 | return es.notify(true, dest, body) 27 | } 28 | 29 | func (es *EmailSender) Release(dest []string, body string) error { 30 | return es.notify(false, dest, body) 31 | } 32 | 33 | func (es *EmailSender) notify(trigger bool, dest []string, body string) error { 34 | event := "Triggered" 35 | if !trigger { 36 | event = "Released" 37 | } 38 | 39 | msg := "From: " + es.From + "\n" + 40 | "Subject: stampzilla - " + event + "\n\n" + 41 | body 42 | 43 | return es.send(fmt.Sprintf("%s:%d", es.Server, es.Port), 44 | smtp.PlainAuth("", es.From, es.Password, es.Server), 45 | es.From, dest, []byte(msg)) 46 | } 47 | 48 | func (es *EmailSender) Destinations() (map[string]string, error) { 49 | return nil, fmt.Errorf("not implemented") 50 | } 51 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "homepage": "https://create-react-app-redux.now.sh", 4 | "scripts": { 5 | "deploy": "now && now alias", 6 | "start": "react-scripts start", 7 | "now-start": "serve -s ./build", 8 | "build": "react-scripts build", 9 | "test": "react-scripts test --env=jsdom", 10 | "eject": "react-scripts eject", 11 | "precommit": "pretty-quick --staged" 12 | }, 13 | "devDependencies": { 14 | "prettier": "1.17.0", 15 | "react-scripts": "^5.0.1", 16 | "terser": "^4.8.1" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.21.2", 20 | "connected-react-router": "6.4.0", 21 | "moment": "^2.29.4", 22 | "react": "16.8.6", 23 | "react-dom": "16.8.6", 24 | "react-moment": "^0.9.2", 25 | "react-redux": "7.0.3", 26 | "react-router": "5.0.0", 27 | "react-router-dom": "5.0.0", 28 | "react-skycons": "^0.7.0", 29 | "reconnectingwebsocket": "^1.0.0", 30 | "redux": "4.0.1", 31 | "redux-define": "^1.1.1", 32 | "redux-immutable": "^4.0.0", 33 | "redux-thunk": "2.3.0", 34 | "sanitize.css": "8.0.0", 35 | "serve": "14.1.2", 36 | "yr.no-forecast": "^2.1.0" 37 | }, 38 | "resolutions": { 39 | "request": "^2.88.0" 40 | }, 41 | "browserslist": [ 42 | ">0.2%", 43 | "not dead", 44 | "not ie <= 11", 45 | "not op_mini all" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/webhook/webhook_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNew(t *testing.T) { 13 | sender := New(json.RawMessage("{\"method\": \"method1\"}")) 14 | 15 | assert.Equal(t, "method1", sender.Method) 16 | } 17 | 18 | func TestTrigger(t *testing.T) { 19 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 20 | assert.Equal(t, "/webhook", req.URL.String()) 21 | rw.Write([]byte(`OK`)) 22 | })) 23 | defer server.Close() 24 | 25 | sender := New(json.RawMessage("{\"method\": \"PUT\"}")) 26 | err := sender.Trigger([]string{server.URL + "/webhook"}, "") 27 | 28 | assert.NoError(t, err) 29 | } 30 | 31 | func TestRelease(t *testing.T) { 32 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 33 | assert.Equal(t, "/webhook", req.URL.String()) 34 | rw.Write([]byte(`OK`)) 35 | })) 36 | defer server.Close() 37 | 38 | sender := New(json.RawMessage("{\"method\": \"PUT\"}")) 39 | err := sender.Release([]string{server.URL + "/webhook"}, "") 40 | 41 | assert.NoError(t, err) 42 | } 43 | 44 | func TestDestinations(t *testing.T) { 45 | sender := New(json.RawMessage("{\"method\": \"PUT\"}")) 46 | d, err := sender.Destinations() 47 | assert.Nil(t, d) 48 | assert.Error(t, err) 49 | } 50 | -------------------------------------------------------------------------------- /nodes/stampzilla-nibe/nibe/parameters.go: -------------------------------------------------------------------------------- 1 | package nibe 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type Parameter struct { 12 | Register string `json:"register"` 13 | Factor int `json:"factor"` 14 | Size string `json:"size"` 15 | Mode string `json:"mode"` 16 | Title string `json:"titel"` 17 | Info string `json:"info"` 18 | Unit string `json:"unit"` 19 | Min string `json:"min"` 20 | Max string `json:"max"` 21 | Map map[string]string `json:"map"` 22 | } 23 | 24 | var parameters = make(map[int]Parameter) 25 | 26 | func (n *Nibe) LoadDefinitions(statikFS http.FileSystem, filename string) error { 27 | jsonFile, err := statikFS.Open(filename) 28 | if err != nil { 29 | return err 30 | } 31 | defer jsonFile.Close() 32 | 33 | byteValue, err := ioutil.ReadAll(jsonFile) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | var decodedParams []Parameter 39 | json.Unmarshal(byteValue, &decodedParams) 40 | 41 | for _, param := range decodedParams { 42 | id, _ := strconv.Atoi(param.Register) 43 | parameters[id] = param 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (n *Nibe) Describe(reg uint16) (*Parameter, error) { 50 | if p, ok := parameters[int(reg)]; ok { 51 | return &p, nil 52 | } 53 | 54 | return nil, fmt.Errorf("Not found") 55 | } 56 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/src/routes/home/DeviceList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | class DeviceList extends React.PureComponent { 5 | render() { 6 | const { devices, state } = this.props 7 | 8 | return ( 9 |
10 | {devices.map(device => { 11 | let value = JSON.stringify( 12 | state.getIn([device.get('device'), 'state', device.get('state')]) 13 | ) 14 | 15 | if (value && device.get('decimals')) { 16 | value *= Math.pow(10, device.get('decimals')) 17 | value = Math.round(value) 18 | value /= Math.pow(10, device.get('decimals')) 19 | } 20 | 21 | if (value && device.get('states')) { 22 | value = device.getIn(['states', value]) 23 | } 24 | 25 | if (device.get('unit')) { 26 | value = `${value} ${device.get('unit')}` 27 | } 28 | 29 | return ( 30 |
33 | {value} 34 | {device.get('title')} 35 |
36 | ) 37 | })} 38 |
39 | ) 40 | } 41 | } 42 | 43 | const mapStateToProps = state => ({ 44 | state: state.getIn(['devices', 'list']) 45 | }) 46 | 47 | export default connect(mapStateToProps)(DeviceList) 48 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type WebhookSender struct { 10 | Method string `json:"method"` 11 | } 12 | 13 | func New(parameters json.RawMessage) *WebhookSender { 14 | ws := &WebhookSender{} 15 | 16 | json.Unmarshal(parameters, ws) 17 | 18 | return ws 19 | } 20 | 21 | func (ws *WebhookSender) Trigger(dest []string, body string) error { 22 | var failure error 23 | for _, url := range dest { 24 | err := ws.notify(true, url, body) 25 | if err != nil { 26 | failure = err 27 | } 28 | } 29 | 30 | return failure 31 | } 32 | 33 | func (ws *WebhookSender) Release(dest []string, body string) error { 34 | var failure error 35 | for _, url := range dest { 36 | err := ws.notify(false, url, body) 37 | if err != nil { 38 | failure = err 39 | } 40 | } 41 | 42 | return failure 43 | } 44 | 45 | func (ws *WebhookSender) notify(trigger bool, url string, body string) error { 46 | req, err := http.NewRequest(ws.Method, url, nil) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | client := &http.Client{} 52 | resp, err := client.Do(req) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | defer resp.Body.Close() 58 | 59 | // b, err := ioutil.ReadAll(resp.Body) 60 | // spew.Dump(b) 61 | 62 | return err 63 | } 64 | 65 | func (ws *WebhookSender) Destinations() (map[string]string, error) { 66 | return nil, fmt.Errorf("not implemented") 67 | } 68 | -------------------------------------------------------------------------------- /nodes/stampzilla-linux/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 9 | "github.com/stampzilla/stampzilla-go/v2/pkg/node" 10 | ) 11 | 12 | type Config struct { 13 | Display string `json:"display"` 14 | Players map[string]PlayerConfig `json:"players"` 15 | } 16 | 17 | var ( 18 | config Config 19 | n *node.Node 20 | ) 21 | 22 | func main() { 23 | n = node.New("linux") 24 | 25 | n.OnConfig(updatedConfig) 26 | n.OnRequestStateChange(func(state devices.State, device *devices.Device) error { 27 | logrus.Info("OnRequestStateChange:", state, device.ID) 28 | 29 | devID := strings.Split(device.ID.ID, ":") 30 | switch devID[0] { 31 | case "monitor": 32 | return changeDpmsState(":"+devID[1], state["on"] == true) 33 | case "player": 34 | return commandPlayer(devID[1], state["on"] == true) 35 | case "audio": 36 | return commandVolume(state) 37 | } 38 | 39 | return nil 40 | }) 41 | 42 | err := n.Connect() 43 | if err != nil { 44 | logrus.Error(err) 45 | return 46 | } 47 | 48 | go startMonitorDpms() 49 | go monitorHealth() 50 | go monitorVolume() 51 | 52 | n.Wait() 53 | } 54 | 55 | func updatedConfig(data json.RawMessage) error { 56 | newConf := Config{} 57 | err := json.Unmarshal(data, &newConf) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | config = newConf 63 | 64 | restartPlayers() 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/lesismal/melody" 8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/interfaces" 9 | ) 10 | 11 | type Message struct { 12 | FromUUID string `json:"fromUUID,omitempty"` 13 | Type string `json:"type"` 14 | Body json.RawMessage `json:"body,omitempty"` 15 | Request json.RawMessage `json:"request,omitempty"` 16 | } 17 | 18 | func NewMessage(t string, body interface{}) (*Message, error) { 19 | b, err := json.Marshal(body) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &Message{ 25 | Type: t, 26 | Body: json.RawMessage(b), 27 | }, nil 28 | } 29 | 30 | func ParseMessage(msg []byte) (*Message, error) { 31 | data := &Message{} 32 | err := json.Unmarshal(msg, data) 33 | return data, err 34 | } 35 | 36 | func (m *Message) WriteTo(s interfaces.MelodyWriter) error { 37 | msg, err := m.Encode() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | return s.Write(msg) 43 | } 44 | 45 | func (m *Message) WriteWithFilter(mel *melody.Melody, f func(s *melody.Session) bool) error { 46 | msg, err := m.Encode() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return mel.BroadcastFilter(msg, f) 52 | } 53 | 54 | func (m *Message) Encode() ([]byte, error) { 55 | msg, err := json.Marshal(m) 56 | 57 | return msg, err 58 | } 59 | 60 | func (m *Message) String() string { 61 | return fmt.Sprintf("Type: %s Body: %s", m.Type, string(m.Body)) 62 | } 63 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/savedstates.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | import { v4 as makeUUID } from 'uuid'; 4 | 5 | const c = defineAction('savedstates', ['ADD', 'SAVE', 'UPDATE', 'REMOVE']); 6 | 7 | const defaultState = Map({ 8 | list: Map(), 9 | }); 10 | 11 | // Actions 12 | export function add(states) { 13 | return { type: c.ADD, states }; 14 | } 15 | export function save(state) { 16 | return { type: c.SAVE, state }; 17 | } 18 | export function remove(uuid) { 19 | return { type: c.REMOVE, uuid }; 20 | } 21 | export function update(states) { 22 | return { type: c.UPDATE, states }; 23 | } 24 | 25 | // Subscribe to channels and register the action for the packages 26 | export function subscribe(dispatch) { 27 | return { 28 | savedstates: (states) => dispatch(update(states)), 29 | }; 30 | } 31 | 32 | // Reducer 33 | export default function reducer(state = defaultState, action) { 34 | switch (action.type) { 35 | case c.ADD: { 36 | const s = { 37 | ...action.state, 38 | uuid: makeUUID(), 39 | }; 40 | return state.setIn(['list', s.uuid], fromJS(s)); 41 | } 42 | case c.SAVE: { 43 | return state.mergeIn(['list', action.state.uuid], fromJS(action.state)); 44 | } 45 | case c.REMOVE: { 46 | return state.deleteIn(['list', action.uuid]); 47 | } 48 | case c.UPDATE: { 49 | return state.set('list', fromJS(action.states)); 50 | } 51 | default: 52 | return state; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/persons.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | import { v4 as makeUUID } from 'uuid'; 4 | 5 | const c = defineAction('persons', ['ADD', 'SAVE', 'REMOVE', 'UPDATE']); 6 | 7 | const defaultState = Map({ 8 | list: Map(), 9 | }); 10 | 11 | // Actions 12 | export function add(person) { 13 | return { type: c.ADD, person }; 14 | } 15 | export function save(person) { 16 | return { type: c.SAVE, person }; 17 | } 18 | export function remove(uuid) { 19 | return { type: c.REMOVE, uuid }; 20 | } 21 | export function update(list) { 22 | return { type: c.UPDATE, list }; 23 | } 24 | 25 | // Subscribe to channels and register the action for the packages 26 | export function subscribe(dispatch) { 27 | return { 28 | persons: (persons) => dispatch(update(persons)), 29 | }; 30 | } 31 | 32 | // Reducer 33 | export default function reducer(state = defaultState, action) { 34 | switch (action.type) { 35 | case c.ADD: { 36 | const person = { 37 | ...action.person, 38 | uuid: makeUUID(), 39 | }; 40 | return state.setIn(['list', person.uuid], fromJS(person)); 41 | } 42 | case c.SAVE: { 43 | return state.mergeIn(['list', action.person.uuid], fromJS(action.person)); 44 | } 45 | case c.REMOVE: { 46 | return state.deleteIn(['list', action.uuid]); 47 | } 48 | case c.UPDATE: { 49 | return state.set('list', fromJS(action.list)); 50 | } 51 | default: 52 | return state; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /nodes/stampzilla-deconz/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var config = &Config{} 13 | 14 | type Config struct { 15 | IP string 16 | Port string 17 | WsPort string 18 | Password string 19 | } 20 | 21 | var localConfig = &LocalConfig{} 22 | 23 | type LocalConfig struct { 24 | APIKey string 25 | } 26 | 27 | func (lc LocalConfig) Save() error { 28 | path := "localconfig.json" 29 | configFile, err := os.Create(path) 30 | if err != nil { 31 | return fmt.Errorf("error saving local config: %s", err.Error()) 32 | } 33 | encoder := json.NewEncoder(configFile) 34 | encoder.SetIndent("", "\t") 35 | err = encoder.Encode(lc) 36 | return errors.Wrap(err, "error saving local config") 37 | } 38 | 39 | func (lc *LocalConfig) Load() error { 40 | path := "localconfig.json" 41 | logrus.Debug("loading local config from ", path) 42 | configFile, err := os.Open(path) 43 | if err != nil { 44 | if os.IsNotExist(err) { 45 | logrus.Warn(err) 46 | return nil 47 | } 48 | return fmt.Errorf("error loading local config: %s", err.Error()) 49 | } 50 | 51 | jsonParser := json.NewDecoder(configFile) 52 | err = jsonParser.Decode(&lc) 53 | return errors.Wrap(err, "error loading local config") 54 | 55 | // TODO loop over rules and generate UUIDs if needed. If it was needed save the rules again 56 | } 57 | 58 | /* 59 | Config to put into gui: 60 | { 61 | "ip":"192.168.13.1", 62 | "port":"9042", 63 | "password":"password" 64 | } 65 | 66 | */ 67 | -------------------------------------------------------------------------------- /nodes/stampzilla-nx-witness/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | type Rule struct { 9 | ActionParams string `json:"actionParams"` 10 | ActionResourceIds []string `json:"actionResourceIds"` 11 | ActionType string `json:"actionType"` 12 | AggregationPeriod int `json:"aggregationPeriod"` 13 | Comment string `json:"comment"` 14 | Disabled bool `json:"disabled"` 15 | EventCondition string `json:"eventCondition"` 16 | EventResourceIds []string `json:"eventResourceIds"` 17 | EventState string `json:"eventState"` 18 | EventType string `json:"eventType"` 19 | ID string `json:"id"` 20 | Schedule string `json:"schedule"` 21 | System bool `json:"system"` 22 | } 23 | 24 | type EventRulesResponse []Rule 25 | 26 | var ErrRuleNotFound = fmt.Errorf("rule not found") 27 | 28 | func (rules EventRulesResponse) ByID(id string) (Rule, error) { 29 | for _, v := range rules { 30 | if v.StampzillaDeviceID() == id { 31 | return v, nil 32 | } 33 | } 34 | return Rule{}, ErrRuleNotFound 35 | } 36 | 37 | /* 38 | those are mandatory when using POST 39 | id uuid (required) 40 | eventType enum (required) 41 | eventState enum (required) 42 | actionType enum (required) 43 | disabled boolean (required) 44 | */ 45 | 46 | var re = regexp.MustCompile(`stampzilla:(.*)`) 47 | 48 | func (r Rule) StampzillaDeviceID() string { 49 | m := re.FindAllStringSubmatch(r.Comment, 1) 50 | if len(m) > 0 && len(m[0]) > 1 { 51 | return m[0][1] 52 | } 53 | return "" 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stampzilla-go [![Build Status](https://app.travis-ci.com/stampzilla/stampzilla-go.svg?branch=master&status=started)](https://travis-ci.org/stampzilla/stampzilla-go) [![codecov](https://codecov.io/gh/stampzilla/stampzilla-go/branch/master/graph/badge.svg)](https://codecov.io/gh/stampzilla/stampzilla-go) [![Go Report Card](https://goreportcard.com/badge/github.com/stampzilla/stampzilla-go)](https://goreportcard.com/report/github.com/stampzilla/stampzilla-go) 2 | ============= 3 | 4 | Awesome homeautomation software written in Go and React 5 | 6 | ### Installing 7 | 8 | Installation from precompiled binaries 9 | ```bash 10 | curl -s https://api.github.com/repos/stampzilla/stampzilla-go/releases/latest | grep "browser_download_url.*stampzilla-linux-amd64" | cut -d : -f 2,3 | tr -d \" | xargs curl -L -s -o stampzilla && chmod +x stampzilla 11 | sudo mv stampzilla /usr/local/bin #or ~/bin if you use that 12 | sudo stampzilla install server deconz #or whatever nodes you want to use. 13 | ``` 14 | 15 | Installation from source 16 | ```bash 17 | go install github.com/stampzilla/stampzilla-go/v2/cmd/stampzilla@latest 18 | sudo stampzilla install 19 | ``` 20 | This creates a stampzilla user. checksout the code in stampzilla user home folder and creates some required folders. 21 | 22 | ### Updating 23 | 24 | Update the cli with `stampzilla self-update` 25 | 26 | Update nodes with 27 | ``` 28 | sudo stampzilla install -u 29 | sudo stampzilla restart 30 | ``` 31 | 32 | ### Documentation 33 | Is work in progress and can be found here: 34 | * [Docs](docs/README.md) 35 | * [Screenshots](docs/screenshots.md) 36 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/index.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/js/bootstrap.bundle'; 2 | import 'admin-lte/dist/js/adminlte'; 3 | import 'core-js/stable'; 4 | import 'regenerator-runtime/runtime'; 5 | import { Provider } from 'react-redux'; 6 | import { render } from 'react-dom'; 7 | import React from 'react'; 8 | import { ToastContainer } from 'react-toastify'; 9 | 10 | import './index.scss'; 11 | import './images/android-chrome-192x192.png'; 12 | import './images/android-chrome-512x512.png'; 13 | import './images/apple-touch-icon.png'; 14 | import './images/browserconfig.xml'; 15 | import './images/favicon-16x16.png'; 16 | import './images/favicon-32x32.png'; 17 | import './images/favicon.ico'; 18 | import './images/safari-pinned-tab.svg'; 19 | import './images/site.webmanifest'; 20 | import ErrorBoundary from './components/ErrorBoundary'; 21 | import Wrapper from './components/Wrapper'; 22 | import store from './store'; 23 | 24 | render( 25 | 26 | 27 | 28 | 29 | 30 | , 31 | document.getElementById('app'), 32 | ); 33 | 34 | /* eslint-disable no-undef */ 35 | if (NODE_ENV === 'production') { 36 | (function registerServiceWorker() { 37 | if ('serviceWorker' in navigator) { 38 | navigator.serviceWorker 39 | .register('service-worker.js', { scope: '/' }) 40 | .then(() => console.log("Service Worker registered successfully.")) // eslint-disable-line 41 | .catch((error) => console.log('Service Worker registration failed:', error), 42 | ); // eslint-disable-line 43 | } 44 | }()); 45 | } 46 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/senders.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | import { v4 as makeUUID } from 'uuid'; 4 | 5 | const c = defineAction( 6 | 'senders', 7 | ['ADD', 'SAVE', 'UPDATE', 'UPDATE_STATE'], 8 | ); 9 | 10 | const defaultState = Map({ 11 | list: Map(), 12 | }); 13 | 14 | // Actions 15 | export function add(sender) { 16 | return { type: c.ADD, sender }; 17 | } 18 | export function save(sender) { 19 | return { type: c.SAVE, sender }; 20 | } 21 | export function update(senders) { 22 | return { type: c.UPDATE, senders }; 23 | } 24 | export function updateState(senders) { 25 | return { type: c.UPDATE_STATE, senders }; 26 | } 27 | 28 | // Subscribe to channels and register the action for the packages 29 | export function subscribe(dispatch) { 30 | return { 31 | senders: senders => dispatch(update(senders)), 32 | }; 33 | } 34 | 35 | // Reducer 36 | export default function reducer(state = defaultState, action) { 37 | switch (action.type) { 38 | case c.ADD: { 39 | const sender = { 40 | ...action.sender, 41 | uuid: makeUUID(), 42 | }; 43 | return state 44 | .setIn(['list', sender.uuid], fromJS(sender)); 45 | } 46 | case c.SAVE: { 47 | return state 48 | .mergeIn(['list', action.sender.uuid], fromJS(action.sender)); 49 | } 50 | case c.UPDATE: { 51 | return state 52 | .set('list', fromJS(action.senders)); 53 | } 54 | case c.UPDATE_STATE: { 55 | return state 56 | .set('state', fromJS(action.senders)); 57 | } 58 | default: return state; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /nodes/stampzilla-linux/volume.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | volume "github.com/itchyny/volume-go" 8 | "github.com/sirupsen/logrus" 9 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 10 | ) 11 | 12 | func monitorVolume() { 13 | dev := &devices.Device{ 14 | Name: "Audio", 15 | ID: devices.ID{ID: "audio"}, 16 | Online: true, 17 | Traits: []string{"OnOff", "Volume"}, 18 | State: devices.State{ 19 | "on": false, 20 | "volume": 0, 21 | }, 22 | } 23 | added := false 24 | 25 | for { 26 | vol, err := volume.GetVolume() 27 | if err != nil { 28 | logrus.Errorf("get volume failed: %+v", err) 29 | return 30 | } 31 | 32 | mute, err := volume.GetMuted() 33 | if err != nil { 34 | logrus.Errorf("get mute failed: %+v", err) 35 | return 36 | } 37 | 38 | if !added { 39 | n.AddOrUpdate(dev) 40 | } 41 | 42 | newState := make(devices.State) 43 | newState["on"] = !mute 44 | newState["volume"] = float64(vol) / 100 45 | n.UpdateState(dev.ID.ID, newState) 46 | <-time.After(time.Second * 1) 47 | } 48 | } 49 | 50 | func commandVolume(state devices.State) error { 51 | if state["volume"] != nil { 52 | err := volume.SetVolume(int(state["volume"].(float64) * 100)) 53 | if err != nil { 54 | return fmt.Errorf("set volume failed: %+v", err) 55 | } 56 | } 57 | 58 | if state["on"] == true { 59 | err := volume.Unmute() 60 | if err != nil { 61 | return fmt.Errorf("mute failed: %+v", err) 62 | } 63 | } else if state["on"] == false { 64 | err := volume.Mute() 65 | if err != nil { 66 | return fmt.Errorf("unmute failed: %+v", err) 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/node/mdns.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/hashicorp/mdns" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // func main() { 14 | // ip, port, err := queryMDNS() 15 | // if err != nil { 16 | // logrus.Error(err) 17 | // return 18 | //} 19 | 20 | // logrus.Infof("Found %s:%d", ip, port) 21 | //} 22 | 23 | func queryMDNS() (string, string, string) { 24 | entriesCh := make(chan *mdns.ServiceEntry) 25 | 26 | logrus.Info("node: running mdns query") 27 | ctx, cancel := context.WithCancel(context.Background()) 28 | go func() { 29 | for { 30 | select { 31 | case <-ctx.Done(): 32 | return 33 | default: 34 | // mdns.Lookup("_stampzilla._tcp", entriesCh) 35 | params := mdns.DefaultParams("_stampzilla._tcp") 36 | params.Entries = entriesCh 37 | params.Timeout = time.Second * 5 38 | params.DisableIPv6 = true 39 | err := mdns.Query(params) 40 | if err != nil { 41 | logrus.Error(err) 42 | } 43 | } 44 | } 45 | }() 46 | 47 | var entry *mdns.ServiceEntry 48 | for { 49 | entry = <-entriesCh 50 | if strings.Contains(entry.Name, "_stampzilla._tcp") { // Ignore answers that are not what we are looking for 51 | break 52 | } 53 | } 54 | cancel() 55 | port := strconv.Itoa(entry.Port) 56 | tlsPort := "" 57 | for _, v := range entry.InfoFields { 58 | tmp := strings.SplitN(v, "=", 2) 59 | if len(tmp) != 2 { 60 | continue 61 | } 62 | if tmp[0] == "tlsPort" { 63 | tlsPort = tmp[1] 64 | } 65 | } 66 | logrus.Infof("node: got mdns query response %s:%s (tlsPort=%s)", entry.AddrV4.String(), port, tlsPort) 67 | return entry.AddrV4.String(), port, tlsPort 68 | } 69 | -------------------------------------------------------------------------------- /nodes/stampzilla-keba-p30/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Report2 struct { 4 | ID string `json:"ID"` 5 | State int `json:"State"` 6 | Error1 int `json:"Error1"` 7 | Error2 int `json:"Error2"` 8 | Plug int `json:"Plug"` 9 | AuthON int `json:"AuthON"` 10 | Authreq int `json:"Authreq"` 11 | EnableSys int `json:"Enable sys"` 12 | EnableUser int `json:"Enable user"` 13 | MaxCurr int `json:"Max curr"` 14 | MaxCurrPercent int `json:"Max curr %"` 15 | CurrHW int `json:"Curr HW"` 16 | CurrUser int `json:"Curr user"` 17 | CurrFS int `json:"Curr FS"` 18 | TmoFS int `json:"Tmo FS"` 19 | CurrTimer int `json:"Curr timer"` 20 | TmoCT int `json:"Tmo CT"` 21 | Setenergy int `json:"Setenergy"` 22 | Output int `json:"Output"` 23 | Input int `json:"Input"` 24 | X2PhaseSwitchSource int `json:"X2 phaseSwitch source"` 25 | X2PhaseSwitch int `json:"X2 phaseSwitch"` 26 | Serial string `json:"Serial"` 27 | Sec int `json:"Sec"` 28 | } 29 | type Report3 struct { 30 | ID string `json:"ID"` 31 | U1 int `json:"U1"` 32 | U2 int `json:"U2"` 33 | U3 int `json:"U3"` 34 | I1 int `json:"I1"` 35 | I2 int `json:"I2"` 36 | I3 int `json:"I3"` 37 | P int `json:"P"` 38 | PF int `json:"PF"` 39 | EPres int `json:"E pres"` 40 | ETotal int `json:"E total"` 41 | Serial string `json:"Serial"` 42 | Sec int `json:"Sec"` 43 | } 44 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "time" 8 | ) 9 | 10 | type FileSender struct { 11 | Append bool `json:"append"` 12 | Timestamp bool `json:"timestamp"` 13 | } 14 | 15 | func New(parameters json.RawMessage) *FileSender { 16 | f := &FileSender{} 17 | 18 | json.Unmarshal(parameters, f) 19 | 20 | return f 21 | } 22 | 23 | func (f *FileSender) Trigger(dest []string, body string) error { 24 | var failure error 25 | for _, d := range dest { 26 | err := f.notify(true, d, body) 27 | if err != nil { 28 | failure = err 29 | } 30 | } 31 | 32 | return failure 33 | } 34 | 35 | func (f *FileSender) Release(dest []string, body string) error { 36 | var failure error 37 | for _, d := range dest { 38 | err := f.notify(false, d, body) 39 | if err != nil { 40 | failure = err 41 | } 42 | } 43 | 44 | return failure 45 | } 46 | 47 | func (f *FileSender) notify(trigger bool, filename string, body string) error { 48 | mode := os.O_CREATE | os.O_WRONLY 49 | if f.Append { 50 | mode = os.O_APPEND | os.O_CREATE | os.O_WRONLY 51 | } 52 | 53 | tf, err := os.OpenFile(filename, mode, 0644) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | defer tf.Close() 59 | 60 | line := body 61 | if f.Timestamp { 62 | line = fmt.Sprintf("%s\t%s", time.Now().Format("2006-01-02 15:04:05"), line) 63 | } 64 | 65 | event := "Triggered" 66 | if !trigger { 67 | event = "Released" 68 | } 69 | line = fmt.Sprintf("%s\t%s\r\n", line, event) 70 | 71 | _, err = tf.WriteString(line) 72 | return err 73 | } 74 | 75 | func (f *FileSender) Destinations() (map[string]string, error) { 76 | return nil, fmt.Errorf("not implemented") 77 | } 78 | -------------------------------------------------------------------------------- /nodes/stampzilla-telldus/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "_cgo_export.h" 6 | 7 | int callbackDeviceEvent = 0; 8 | int callbackDeviceChangeEvent = 0; 9 | int callbackRawDeviceEvent = 0; 10 | int callbackSensorEvent = 0; 11 | 12 | void registerCallbacks() { 13 | tdInit(); 14 | 15 | callbackSensorEvent = tdRegisterSensorEvent( (TDSensorEvent)&sensorEvent, 0 ); 16 | callbackDeviceEvent = tdRegisterDeviceEvent( (TDDeviceEvent)&deviceEvent, 0 ); 17 | callbackDeviceChangeEvent = tdRegisterDeviceChangeEvent( (TDDeviceChangeEvent)&deviceChangeEvent, 0 ); 18 | callbackRawDeviceEvent = tdRegisterRawDeviceEvent( (TDRawDeviceEvent)&rawDeviceEvent, 0 ); 19 | } 20 | 21 | void unregisterCallbacks() { 22 | tdUnregisterCallback( callbackSensorEvent ); 23 | tdUnregisterCallback( callbackDeviceEvent); 24 | tdUnregisterCallback( callbackDeviceChangeEvent ); 25 | tdUnregisterCallback( callbackRawDeviceEvent ); 26 | tdClose(); 27 | } 28 | 29 | int updateDevices() { 30 | int intNumberOfDevices = tdGetNumberOfDevices(); 31 | int i; 32 | 33 | for (i = 0; i < intNumberOfDevices; i++) { 34 | int id = tdGetDeviceId( i ); 35 | char *name = tdGetName( id ); 36 | int methods = tdMethods(id, TELLSTICK_TURNON | TELLSTICK_TURNOFF | TELLSTICK_BELL | TELLSTICK_TOGGLE | TELLSTICK_DIM | TELLSTICK_EXECUTE | TELLSTICK_UP | TELLSTICK_DOWN | TELLSTICK_STOP ); 37 | 38 | int state = tdLastSentCommand( id, TELLSTICK_TURNON | TELLSTICK_TURNOFF | TELLSTICK_DIM ); 39 | char *value = tdLastSentValue( id ); 40 | 41 | newDevice(id,name,methods,state,value); 42 | 43 | tdReleaseString(name); 44 | } 45 | 46 | return intNumberOfDevices; 47 | } 48 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/destinations.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | import { v4 as makeUUID } from 'uuid'; 4 | 5 | const c = defineAction('destinations', [ 6 | 'ADD', 7 | 'SAVE', 8 | 'UPDATE', 9 | 'UPDATE_STATE', 10 | ]); 11 | 12 | const defaultState = Map({ 13 | list: Map(), 14 | }); 15 | 16 | // Actions 17 | export function add(destination) { 18 | return { type: c.ADD, destination }; 19 | } 20 | export function save(destination) { 21 | return { type: c.SAVE, destination }; 22 | } 23 | export function update(destinations) { 24 | return { type: c.UPDATE, destinations }; 25 | } 26 | export function updateState(destinations) { 27 | return { type: c.UPDATE_STATE, destinations }; 28 | } 29 | 30 | // Subscribe to channels and register the action for the packages 31 | export function subscribe(dispatch) { 32 | return { 33 | destinations: destinations => dispatch(update(destinations)), 34 | }; 35 | } 36 | 37 | // Reducer 38 | export default function reducer(state = defaultState, action) { 39 | switch (action.type) { 40 | case c.ADD: { 41 | const destination = { 42 | ...action.destination, 43 | uuid: makeUUID(), 44 | }; 45 | return state.setIn(['list', destination.uuid], fromJS(destination)); 46 | } 47 | case c.SAVE: { 48 | return state.mergeIn( 49 | ['list', action.destination.uuid], 50 | fromJS(action.destination), 51 | ); 52 | } 53 | case c.UPDATE: { 54 | return state.set('list', fromJS(action.destinations)); 55 | } 56 | case c.UPDATE_STATE: { 57 | return state.set('state', fromJS(action.destinations)); 58 | } 59 | default: 60 | return state; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /nodes/stampzilla-magicmirror/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 23 | stampzilla magic mirror 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/email/email_test.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "encoding/json" 5 | "net/smtp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | sender := New(json.RawMessage("{\"server\": \"server1\", \"port\": 123, \"from\": \"from1\", \"password\": \"pass1\"}")) 13 | 14 | assert.Equal(t, "server1", sender.Server) 15 | assert.Equal(t, 123, sender.Port) 16 | assert.Equal(t, "from1", sender.From) 17 | assert.Equal(t, "pass1", sender.Password) 18 | } 19 | 20 | func TestTrigger(t *testing.T) { 21 | f, r := mockSend(nil) 22 | sender := &EmailSender{send: f} 23 | err := sender.Trigger([]string{"me@example.com"}, "Hello World") 24 | 25 | assert.NoError(t, err) 26 | assert.Equal(t, "From: \nSubject: stampzilla - Triggered\n\nHello World", string(r.msg)) 27 | } 28 | 29 | func TestRelease(t *testing.T) { 30 | f, r := mockSend(nil) 31 | sender := &EmailSender{send: f} 32 | err := sender.Release([]string{"me@example.com"}, "Hello World") 33 | 34 | assert.NoError(t, err) 35 | assert.Equal(t, "From: \nSubject: stampzilla - Released\n\nHello World", string(r.msg)) 36 | } 37 | 38 | func TestDestinations(t *testing.T) { 39 | sender := &EmailSender{} 40 | d, err := sender.Destinations() 41 | assert.Nil(t, d) 42 | assert.Error(t, err) 43 | } 44 | 45 | func mockSend(errToReturn error) (func(string, smtp.Auth, string, []string, []byte) error, *emailRecorder) { 46 | r := new(emailRecorder) 47 | return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error { 48 | *r = emailRecorder{addr, a, from, to, msg} 49 | return errToReturn 50 | }, r 51 | } 52 | 53 | type emailRecorder struct { 54 | addr string 55 | auth smtp.Auth 56 | from string 57 | to []string 58 | msg []byte 59 | } 60 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/sender_test.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestCreateFileSender(t *testing.T) { 14 | s := &Sender{ 15 | Type: "file", 16 | } 17 | 18 | d1 := &Destination{ 19 | Name: "name", 20 | UUID: "uuid", 21 | } 22 | 23 | err := s.Trigger(d1, "test") 24 | assert.NoError(t, err) 25 | 26 | err = s.Release(d1, "test") 27 | assert.NoError(t, err) 28 | 29 | d, err := s.Destinations() 30 | assert.Error(t, err) 31 | assert.Nil(t, d) 32 | } 33 | 34 | func TestCreateUnknownSender(t *testing.T) { 35 | s := &Sender{ 36 | Type: "unknown", 37 | } 38 | 39 | d1 := &Destination{ 40 | Name: "name", 41 | UUID: "uuid", 42 | } 43 | 44 | err := s.Trigger(d1, "test") 45 | assert.Error(t, err) 46 | 47 | err = s.Release(d1, "test") 48 | assert.Error(t, err) 49 | 50 | d, err := s.Destinations() 51 | assert.Error(t, err) 52 | assert.Nil(t, d) 53 | } 54 | 55 | func TestReadWriteSenders(t *testing.T) { 56 | file, err := ioutil.TempFile("", "readwritesenders") 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | defer os.Remove(file.Name()) 61 | 62 | s1 := NewSenders() 63 | s2 := NewSenders() 64 | 65 | sender1 := Sender{ 66 | Name: "name1", 67 | UUID: "uuid1", 68 | Type: "type1", 69 | Parameters: json.RawMessage("null"), 70 | } 71 | s1.Add(sender1) 72 | 73 | err = s1.Save(file.Name()) 74 | assert.NoError(t, err) 75 | 76 | err = s2.Load(file.Name()) 77 | assert.NoError(t, err) 78 | 79 | sender2, ok := s2.Get("uuid1") 80 | assert.True(t, ok) 81 | assert.Equal(t, sender1, sender2) 82 | 83 | assert.Len(t, s2.All(), 1) 84 | 85 | s2.Remove("uuid1") 86 | assert.Len(t, s2.All(), 0) 87 | } 88 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import Immutable from 'immutable'; 3 | import ReduxThunk from 'redux-thunk'; 4 | import persistState from 'redux-localstorage'; 5 | 6 | import destinations from './middlewares/destinations'; 7 | import persons from './middlewares/persons'; 8 | import rootReducer from './ducks'; 9 | import rules from './middlewares/rules'; 10 | import savedstates from './middlewares/savedstates'; 11 | import schedules from './middlewares/schedules'; 12 | import senders from './middlewares/senders'; 13 | import toast from './middlewares/toast'; 14 | 15 | const middleware = [ 16 | toast, 17 | ReduxThunk, 18 | rules, 19 | persons, 20 | destinations, 21 | senders, 22 | schedules, 23 | savedstates, 24 | ]; 25 | 26 | const preloadedState = undefined; 27 | 28 | const composeEnhancers = (typeof window !== 'undefined' 29 | && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) // eslint-disable-line no-underscore-dangle 30 | || compose; 31 | 32 | const localStorageConfig = { 33 | slicer: (paths) => (state) => (paths ? state.filter((v, k) => paths.indexOf(k) > -1) : state), 34 | serialize: (subset) => JSON.stringify(subset.toJS()), 35 | deserialize: (serializedData) => Immutable.fromJS(JSON.parse(serializedData)) || Immutable.Map(), 36 | merge: (initialState, persistedState) => (initialState ? initialState.mergeDeep(persistedState) : persistedState), 37 | }; 38 | 39 | const newStore = (initialState, extraMiddlewares = []) => createStore( 40 | rootReducer, 41 | initialState, 42 | composeEnhancers( 43 | applyMiddleware(...extraMiddlewares, ...middleware), 44 | persistState(['app'], localStorageConfig), 45 | ), 46 | ); 47 | const store = newStore(preloadedState); 48 | 49 | export default store; 50 | export { newStore }; 51 | -------------------------------------------------------------------------------- /nodes/stampzilla-modbus/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "device":"/dev/ttyUSB0", 4 | "registers": { 5 | "outsideTemp": { 6 | "Name": "outSideTemp", 7 | "Id": 218, 8 | "Base": 10 9 | }, 10 | "supplyAirTemp": { 11 | "Name": "supplyTemp", 12 | "Id": 214, 13 | "Base": 10 14 | }, 15 | "exitAirTemp": { 16 | "Name": "exitAirTemp", 17 | "Id": 215, 18 | "Base": 10 19 | }, 20 | "coolerSignal": { 21 | "Name": "coolerSignal", 22 | "Id": 204 23 | }, 24 | "bypassSignal": { 25 | "Name": "bypassSignal", 26 | "Id": 301 27 | }, 28 | "fanSpeed": { 29 | "Name": "fanSpeed", 30 | "Id": 101 31 | }, 32 | "defrostState": { 33 | "Name": "defrostState", 34 | "Id": 651 35 | }, 36 | "defrostConfig": { 37 | "Name": "defrostConfig", 38 | "Id": 652 39 | }, 40 | "defrostMode": { 41 | "Name": "defrostMode", 42 | "Id": 654 43 | }, 44 | "supplyFanRpm": { 45 | "Name": "supplyFanRpm", 46 | "Id": 111 47 | }, 48 | "exitFanRpm": { 49 | "Name": "exitFanRpm", 50 | "Id": 112 51 | }, 52 | "supplyFanPwm": { 53 | "Name": "supplyFanPwm", 54 | "Id": 109 55 | }, 56 | "exitFanPwm": { 57 | "Name": "exitFanPwm", 58 | "Id": 110 59 | }, 60 | "preHeaterRelays": { 61 | "Name": "preHeaterRelays", 62 | "Id": 751 63 | }, 64 | "alarms": { 65 | "Name": "alarms", 66 | "Id": 801 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/rules.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | import { v4 as makeUUID } from 'uuid'; 4 | 5 | const c = defineAction('rules', [ 6 | 'ADD', 7 | 'SAVE', 8 | 'REMOVE', 9 | 'UPDATE', 10 | 'UPDATE_STATE', 11 | ]); 12 | 13 | const defaultState = Map({ 14 | list: Map(), 15 | state: Map(), 16 | }); 17 | 18 | // Actions 19 | export function add(rule) { 20 | return { type: c.ADD, rule }; 21 | } 22 | export function save(rule) { 23 | return { type: c.SAVE, rule }; 24 | } 25 | export function remove(uuid) { 26 | return { type: c.REMOVE, uuid }; 27 | } 28 | export function update(rules) { 29 | return { type: c.UPDATE, rules }; 30 | } 31 | export function updateState(rules) { 32 | return { type: c.UPDATE_STATE, rules }; 33 | } 34 | 35 | // Subscribe to channels and register the action for the packages 36 | export function subscribe(dispatch) { 37 | return { 38 | rules: (rules) => dispatch(update(rules)), 39 | server: ({ rules }) => rules && dispatch(updateState(rules)), 40 | }; 41 | } 42 | 43 | // Reducer 44 | export default function reducer(state = defaultState, action) { 45 | switch (action.type) { 46 | case c.ADD: { 47 | const rule = { 48 | ...action.rule, 49 | uuid: makeUUID(), 50 | }; 51 | return state.setIn(['list', rule.uuid], fromJS(rule)); 52 | } 53 | case c.SAVE: { 54 | return state.mergeIn(['list', action.rule.uuid], fromJS(action.rule)); 55 | } 56 | case c.REMOVE: { 57 | return state.deleteIn(['list', action.uuid]); 58 | } 59 | case c.UPDATE: { 60 | return state.set('list', fromJS(action.rules)); 61 | } 62 | case c.UPDATE_STATE: { 63 | return state.set('state', fromJS(action.rules)); 64 | } 65 | default: 66 | return state; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/Link.js: -------------------------------------------------------------------------------- 1 | import { Link as RouterLink } from 'react-router-dom'; 2 | import { withRouter } from 'react-router'; 3 | import React from 'react'; 4 | import Url from 'url'; 5 | import classnames from 'classnames'; 6 | 7 | class Link extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | isLocal: false, 12 | }; 13 | } 14 | 15 | /* eslint-disable react/no-did-mount-set-state */ 16 | componentDidMount() { 17 | const { to } = this.props; 18 | if (to && typeof window !== 'undefined') { 19 | const url = Url.parse(to); 20 | if ( 21 | window.location.hostname === url.hostname 22 | || !url.hostname 23 | || !url.hostname.length 24 | ) { 25 | this.setState({ 26 | isLocal: true, 27 | localTo: to.replace('www.', '').replace(window.location.origin, ''), 28 | }); 29 | } 30 | } 31 | } 32 | /* eslint-enable */ 33 | 34 | render() { 35 | const { 36 | to, 37 | className, 38 | children, 39 | location, 40 | activeClass, 41 | onClick, 42 | exact, 43 | } = this.props; 44 | const { isLocal, localTo } = this.state; 45 | const active = localTo 46 | && activeClass 47 | && ((location.pathname.substring(0, localTo.length) === localTo && !exact) 48 | || (location.pathname === localTo && exact)) 49 | ? activeClass 50 | : null; 51 | 52 | return isLocal ? ( 53 | 58 | {children} 59 | 60 | ) : ( 61 | 62 | {children} 63 | 64 | ); 65 | } 66 | } 67 | 68 | export default withRouter(Link); 69 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/websocket/websocket.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/lesismal/melody" 5 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models" 6 | ) 7 | 8 | type SessionKey string 9 | 10 | const ( 11 | KeyProtocol SessionKey = "protocol" 12 | KeyID SessionKey = "ID" 13 | ) 14 | 15 | func (sk SessionKey) String() string { 16 | return string(sk) 17 | } 18 | 19 | type Sender interface { 20 | SendToID(to string, msgType string, data interface{}) error 21 | SendToProtocol(to string, msgType string, data interface{}) error 22 | BroadcastWithFilter(msgType string, data interface{}, fn func(*melody.Session) bool) error 23 | } 24 | 25 | type sender struct { 26 | Melody *melody.Melody 27 | } 28 | 29 | func NewWebsocketSender(m *melody.Melody) Sender { 30 | return &sender{ 31 | Melody: m, 32 | } 33 | } 34 | 35 | func (ws *sender) sendMessageTo(key SessionKey, to string, msg *models.Message) error { 36 | return msg.WriteWithFilter(ws.Melody, func(s *melody.Session) bool { 37 | v, exists := s.Get(key.String()) 38 | return exists && v == to 39 | }) 40 | } 41 | 42 | func (ws *sender) SendToID(to string, msgType string, data interface{}) error { 43 | message, err := models.NewMessage(msgType, data) 44 | if err != nil { 45 | return err 46 | } 47 | return ws.sendMessageTo(KeyID, to, message) 48 | } 49 | 50 | func (ws *sender) SendToProtocol(to string, msgType string, data interface{}) error { 51 | message, err := models.NewMessage(msgType, data) 52 | if err != nil { 53 | return err 54 | } 55 | return ws.sendMessageTo(KeyProtocol, to, message) 56 | } 57 | 58 | func (ws *sender) BroadcastWithFilter(msgType string, data interface{}, fn func(*melody.Session) bool) error { 59 | message, err := models.NewMessage(msgType, data) 60 | if err != nil { 61 | return err 62 | } 63 | return message.WriteWithFilter(ws.Melody, fn) 64 | } 65 | -------------------------------------------------------------------------------- /nodes/stampzilla-spc/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/e2e" 9 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestUpdateStateFromUDP(t *testing.T) { 14 | // logrus.SetLevel(logrus.DebugLevel) 15 | main, _, cleanup := e2e.SetupWebsocketTest(t) 16 | defer cleanup() 17 | e2e.AcceptCertificateRequest(t, main) 18 | 19 | config.EDPPort = "9999" 20 | _, node, listenPort := start() 21 | listenPort <- config.EDPPort 22 | 23 | time.Sleep(time.Millisecond * 100) // Wait for udp server to start 24 | err := writeUDP(config.EDPPort) 25 | assert.NoError(t, err) 26 | 27 | e2e.WaitFor(t, 1*time.Second, "we should have 1 device", func() bool { 28 | return len(main.Store.GetDevices().All()) == 1 29 | }) 30 | // spew.Dump(main.Store.Devices.All()) 31 | // spew.Dump(node.Devices.All()) 32 | 33 | // Assert that the device exists in the server after we got UDP packet 34 | assert.Equal(t, "Zone Kök IR", main.Store.GetDevices().Get(devices.ID{ID: "zone.8", Node: node.UUID}).Name) 35 | assert.Equal(t, "Zone Kök IR", main.Store.GetDevices().Get(devices.ID{ID: "zone.8", Node: node.UUID}).Name) 36 | } 37 | 38 | func writeUDP(port string) error { 39 | d := []byte{0x45, 0x2, 0x0, 0x3e, 0x0, 0x0, 0x0, 0xe8, 0x3, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x95, 0xa1, 0x33, 0x0, 0x45, 0x32, 0x5b, 0x23, 0x31, 0x30, 0x30, 0x30, 0x7c, 0x32, 0x31, 0x31, 0x35, 0x35, 0x37, 0x30, 0x33, 0x31, 0x31, 0x32, 0x30, 0x32, 0x30, 0x7c, 0x5a, 0x4f, 0x7c, 0x38, 0x7c, 0x4b, 0xf6, 0x6b, 0x20, 0x49, 0x52, 0xa6, 0x5a, 0x4f, 0x4e, 0x45, 0xa6, 0x31, 0xa6, 0x4c, 0x61, 0x72, 0x6d, 0x7c, 0x7c, 0x30, 0x5d} 40 | conn, err := net.Dial("udp", "127.0.0.1:"+port) 41 | if err != nil { 42 | return err 43 | } 44 | _, err = conn.Write(d) 45 | return err 46 | } 47 | -------------------------------------------------------------------------------- /nodes/stampzilla-chromecast/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/stampzilla/gocast/discovery" 11 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 12 | "github.com/stampzilla/stampzilla-go/v2/pkg/node" 13 | ) 14 | 15 | var chromecasts = &State{ 16 | Chromecasts: make(map[string]*Chromecast), 17 | } 18 | 19 | func main() { 20 | node := node.New("chromecast") 21 | 22 | // node.OnConfig(updatedConfig) 23 | 24 | ctx, cancel := context.WithCancel(context.Background()) 25 | node.OnShutdown(func() { 26 | cancel() 27 | }) 28 | node.OnRequestStateChange(stateChange) 29 | 30 | if err := node.Connect(); err != nil { 31 | logrus.Error(err) 32 | return 33 | } 34 | 35 | discovery := discovery.NewService() 36 | go discoveryListner(ctx, node, discovery) 37 | discovery.Start(ctx, time.Second*10) 38 | 39 | node.Wait() 40 | } 41 | func stateChange(state devices.State, device *devices.Device) error { 42 | var err error 43 | state.String("say", func(text string) { 44 | cc := chromecasts.GetByUUID(device.ID.ID) 45 | if cc != nil { 46 | base := "https://translate.google.com/translate_tts?client=tw-ob&ie=UTF-8&q=%s&tl=%s" 47 | u := fmt.Sprintf(base, url.QueryEscape(text), url.QueryEscape("sv")) 48 | cc.PlayURL(u, "audio/mpeg") 49 | } 50 | }) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return err 56 | } 57 | 58 | func discoveryListner(ctx context.Context, node *node.Node, discovery *discovery.Service) { 59 | for { 60 | select { 61 | case device := <-discovery.Found(): 62 | logrus.Debugf("New device discovered: %s", device.String()) 63 | d := NewChromecast(node, device) 64 | chromecasts.Add(d) 65 | err := device.Connect(ctx) 66 | if err != nil { 67 | logrus.Error(err) 68 | } 69 | case <-ctx.Done(): 70 | return 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/destinations.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/notification" 8 | ) 9 | 10 | func (store *Store) GetDestinations() map[string]*notification.Destination { 11 | store.RLock() 12 | defer store.RUnlock() 13 | return store.Destinations.All() 14 | } 15 | 16 | func (store *Store) AddOrUpdateDestination(dest *notification.Destination) { 17 | if dest == nil { 18 | return 19 | } 20 | 21 | oldDest := store.Destinations.Get(dest.UUID) 22 | if oldDest != nil && oldDest.Equal(dest) { 23 | return 24 | } 25 | 26 | store.Destinations.Add(dest) 27 | err := store.Destinations.Save("destinations.json") 28 | if err != nil { 29 | logrus.Error(err) 30 | return 31 | } 32 | store.runCallbacks("destinations") 33 | } 34 | 35 | func (store *Store) TriggerDestination(dest string, body string) error { 36 | destination := store.Destinations.Get(dest) 37 | if destination == nil { 38 | return fmt.Errorf("destination definition not found") 39 | } 40 | 41 | sender, ok := store.Senders.Get(destination.Sender) 42 | if !ok { 43 | return fmt.Errorf("sender not found") 44 | } 45 | 46 | return sender.Trigger(destination, body) 47 | } 48 | 49 | func (store *Store) ReleaseDestination(dest string, body string) error { 50 | destination := store.Destinations.Get(dest) 51 | if destination == nil { 52 | return fmt.Errorf("destination definition not found") 53 | } 54 | 55 | sender, ok := store.Senders.Get(destination.Sender) 56 | if !ok { 57 | return fmt.Errorf("sender not found") 58 | } 59 | 60 | return sender.Release(destination, body) 61 | } 62 | 63 | func (store *Store) GetSenderDestinations(id string) (map[string]string, error) { 64 | sender, ok := store.Senders.Get(id) 65 | if !ok { 66 | return nil, fmt.Errorf("sender not found") 67 | } 68 | 69 | return sender.Destinations() 70 | } 71 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/routes/automation/components/SavedStatePicker.scss: -------------------------------------------------------------------------------- 1 | .saved-state-picker { 2 | background: #f0f0f0; 3 | 4 | font-weight: 400; 5 | color: #212529; 6 | vertical-align: middle; 7 | user-select: none; 8 | border: 1px solid transparent; 9 | padding: 0.375rem 0.75rem; 10 | font-size: 1rem; 11 | line-height: 1.5; 12 | border-radius: 0.25rem; 13 | } 14 | 15 | .saved-state-modal { 16 | .modal-content { 17 | border-top: none rgb(222, 226, 230) !important; 18 | border-right-color: rgb(222, 226, 230) !important; 19 | border-bottom-color: rgb(222, 226, 230) !important; 20 | border-left-color: rgb(222, 226, 230) !important; 21 | } 22 | } 23 | 24 | 25 | 26 | :global { 27 | .ReactModal__Overlay { 28 | -webkit-perspective: 600; 29 | perspective: 600; 30 | opacity: 0; 31 | overflow-x: hidden; 32 | overflow-y: scroll !important; 33 | background-color: rgba(0, 0, 0, 0.5); 34 | z-index: 20000; 35 | } 36 | 37 | .ReactModal__Overlay--after-open { 38 | opacity: 1; 39 | transition: opacity 150ms ease-out; 40 | } 41 | 42 | .ReactModal__Content { 43 | -webkit-transform: translateY(30px); 44 | transform: translateY(30px); 45 | z-index: 20000; 46 | 47 | .modal-content{ 48 | .nav-link { 49 | cursor:pointer; 50 | outline: none; 51 | } 52 | } 53 | } 54 | 55 | .ReactModal__Content--after-open { 56 | -webkit-transform: translateY(0); 57 | transform: translateY(0); 58 | transition: all 150ms ease-in; 59 | } 60 | 61 | .ReactModal__Overlay--before-close { 62 | opacity: 0; 63 | } 64 | 65 | .ReactModal__Content--before-close { 66 | -webkit-transform: translateY(30px); 67 | transform: translateY(30px); 68 | transition: all 150ms ease-in; 69 | } 70 | 71 | .ReactModal__Content.modal-dialog { 72 | border: none; 73 | background-color: transparent; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/wirepusher/wirepusher_test.go: -------------------------------------------------------------------------------- 1 | package wirepusher 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNew(t *testing.T) { 13 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}")) 14 | 15 | assert.Equal(t, "title1", sender.Title) 16 | assert.Equal(t, "type1", sender.Type) 17 | assert.Equal(t, "action1", sender.Action) 18 | } 19 | 20 | func TestTrigger(t *testing.T) { 21 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 22 | assert.Equal(t, "/send?action=action1&id=deviceId1&message=test+body&title=title1+-+Triggered&type=type1", req.URL.String()) 23 | rw.Write([]byte(`OK`)) 24 | })) 25 | defer server.Close() 26 | 27 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}")) 28 | sender.url = server.URL + "/send" 29 | err := sender.Trigger([]string{"deviceId1"}, "test body") 30 | 31 | assert.NoError(t, err) 32 | } 33 | 34 | func TestRelease(t *testing.T) { 35 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 36 | assert.Equal(t, "/send?action=action1&id=deviceId1&message=test+body&title=title1+-+Released&type=type1", req.URL.String()) 37 | rw.Write([]byte(`OK`)) 38 | })) 39 | defer server.Close() 40 | 41 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}")) 42 | sender.url = server.URL + "/send" 43 | err := sender.Release([]string{"deviceId1"}, "test body") 44 | 45 | assert.NoError(t, err) 46 | } 47 | 48 | func TestDestinations(t *testing.T) { 49 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}")) 50 | d, err := sender.Destinations() 51 | assert.Nil(t, d) 52 | assert.Error(t, err) 53 | } 54 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/ducks/schedules.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable'; 2 | import { defineAction } from 'redux-define'; 3 | import { v4 as makeUUID } from 'uuid'; 4 | 5 | const c = defineAction( 6 | 'schedules', 7 | ['ADD', 'SAVE', 'REMOVE', 'UPDATE', 'UPDATE_STATE'], 8 | ); 9 | 10 | const defaultState = Map({ 11 | list: Map(), 12 | state: Map(), 13 | }); 14 | 15 | // Actions 16 | export function add(schedule) { 17 | return { type: c.ADD, schedule }; 18 | } 19 | export function save(schedule) { 20 | return { type: c.SAVE, schedule }; 21 | } 22 | export function remove(uuid) { 23 | return { type: c.REMOVE, uuid }; 24 | } 25 | export function update(schedules) { 26 | return { type: c.UPDATE, schedules }; 27 | } 28 | export function updateState(schedules) { 29 | return { type: c.UPDATE_STATE, schedules }; 30 | } 31 | 32 | // Subscribe to channels and register the action for the packages 33 | export function subscribe(dispatch) { 34 | return { 35 | schedules: (schedules) => dispatch(update(schedules)), 36 | server: ({ schedules }) => schedules && dispatch(updateState(schedules)), 37 | }; 38 | } 39 | 40 | // Reducer 41 | export default function reducer(state = defaultState, action) { 42 | switch (action.type) { 43 | case c.ADD: { 44 | const schedule = { 45 | ...action.schedule, 46 | uuid: makeUUID(), 47 | }; 48 | return state 49 | .setIn(['list', schedule.uuid], fromJS(schedule)); 50 | } 51 | case c.SAVE: { 52 | return state 53 | .mergeIn(['list', action.schedule.uuid], fromJS(action.schedule)); 54 | } 55 | case c.REMOVE: { 56 | return state.deleteIn(['list', action.uuid]); 57 | } 58 | case c.UPDATE: { 59 | return state 60 | .set('list', fromJS(action.schedules)); 61 | } 62 | case c.UPDATE_STATE: { 63 | return state 64 | .set('state', fromJS(action.schedules)); 65 | } 66 | default: return state; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /nodes/stampzilla-exoline/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "interval": "30s", 3 | "host": "192.168.13.57:26486", 4 | "variables": [ 5 | { 6 | "name": "ExtractAir", 7 | "loadNumber": 58, 8 | "cell": 585, 9 | "type": "float" 10 | }, 11 | { 12 | "name": "OutdoorAir", 13 | "loadNumber": 58, 14 | "cell": 579, 15 | "type": "float" 16 | }, 17 | { 18 | "name": "ExhaustAir", 19 | "loadNumber": 58, 20 | "cell": 588, 21 | "type": "float" 22 | }, 23 | { 24 | "name": "SupplyAir", 25 | "loadNumber": 58, 26 | "cell": 582, 27 | "type": "float" 28 | }, 29 | { 30 | "name": "SetSupplyAir", 31 | "loadNumber": 4, 32 | "cell": 8, 33 | "type": "float", 34 | "write": true 35 | }, 36 | { 37 | "name": "HeatingValve", 38 | "loadNumber": 4, 39 | "cell": 60, 40 | "type": "float" 41 | }, 42 | { 43 | "name": "CoolingValve", 44 | "loadNumber": 4, 45 | "cell": 63, 46 | "type": "float" 47 | }, 48 | { 49 | "name": "Bypass", 50 | "loadNumber": 4, 51 | "cell": 66, 52 | "type": "float" 53 | }, 54 | { 55 | "name": "SupplyFan", 56 | "loadNumber": 4, 57 | "cell": 69, 58 | "type": "float" 59 | }, 60 | { 61 | "name": "ExtractFan", 62 | "loadNumber": 4, 63 | "cell": 72, 64 | "type": "float" 65 | }, 66 | { 67 | "name": "FanMode", 68 | "loadNumber": 4, 69 | "cell": 42, 70 | "type": "int", 71 | "write": true 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /nodes/stampzilla-knx/setup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 5 | "github.com/stampzilla/stampzilla-go/v2/pkg/node" 6 | ) 7 | 8 | func setupLight(node *node.Node, tunnel *tunnel, light light) { 9 | traits := []string{} 10 | 11 | if light.ControlSwitch != "" { 12 | traits = append(traits, "OnOff") 13 | } 14 | if light.ControlBrightness != "" { 15 | traits = append(traits, "Brightness") 16 | } 17 | 18 | dev := &devices.Device{ 19 | Name: light.ID, 20 | ID: devices.ID{ID: "light." + light.ID}, 21 | Traits: traits, 22 | State: devices.State{ 23 | "on": false, 24 | }, 25 | } 26 | node.AddOrUpdate(dev) 27 | 28 | if light.StateSwitch != "" { 29 | tunnel.AddLink(light.StateSwitch, "on", "bool", dev) 30 | } 31 | 32 | if light.StateBrightness != "" { 33 | tunnel.AddLink(light.StateBrightness, "brightness", "procentage", dev) 34 | } 35 | } 36 | 37 | func setupSensor(node *node.Node, tunnel *tunnel, sensor sensor) { 38 | dev := &devices.Device{ 39 | Name: sensor.ID, 40 | ID: devices.ID{ID: "sensor." + sensor.ID}, 41 | State: make(devices.State), 42 | } 43 | node.AddOrUpdate(dev) 44 | 45 | if sensor.Temperature != "" { 46 | tunnel.AddLink(sensor.Temperature, "temperature", "temperature", dev) 47 | } 48 | if sensor.Motion != "" { 49 | tunnel.AddLink(sensor.Motion, "motion", "bool", dev) 50 | } 51 | if sensor.MotionTrue != "" { 52 | tunnel.AddLink(sensor.Motion, "motionTrue", "bool", dev) 53 | } 54 | if sensor.Lux != "" { 55 | tunnel.AddLink(sensor.Lux, "lux", "lux", dev) 56 | } 57 | if sensor.Humidity != "" { 58 | tunnel.AddLink(sensor.Humidity, "humidity", "humidity", dev) 59 | } 60 | if sensor.Co2 != "" { 61 | tunnel.AddLink(sensor.Co2, "co2", "co2", dev) 62 | } 63 | if sensor.Voc != "" { 64 | tunnel.AddLink(sensor.Voc, "voc", "voc", dev) 65 | } 66 | if sensor.DewPoint != "" { 67 | tunnel.AddLink(sensor.DewPoint, "dewpoint", "dewPoint", dev) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/store/request.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type Request struct { 4 | Identity string `json:"identity"` 5 | Subject RequestSubject `json:"subject"` 6 | Connection string `json:"connection"` 7 | Type string `json:"type"` 8 | Version string `json:"version"` 9 | 10 | Approved chan bool `json:"-"` 11 | } 12 | 13 | type RequestSubject struct { 14 | CommonName string `json:"common_name,omitempty"` 15 | SerialNumber string `json:"serial_number,omitempty"` 16 | Country []string `json:"country,omitempty"` 17 | Organization []string `json:"organization,omitempty"` 18 | OrganizationalUnit []string `json:"organizational_unit,omitempty"` 19 | Locality []string `json:"locality,omitempty"` 20 | Province []string `json:"province,omitempty"` 21 | StreetAddress []string `json:"street_address,omitempty"` 22 | PostalCode []string `json:"postal_code,omitempty"` 23 | Names []interface{} `json:"names,omitempty"` 24 | // ExtraNames []interface{} `json:"extra_names,omitempty"` 25 | } 26 | 27 | func (store *Store) GetRequests() []Request { 28 | store.RLock() 29 | defer store.RUnlock() 30 | return store.Requests 31 | } 32 | 33 | func (store *Store) AddRequest(r Request) { 34 | store.Lock() 35 | store.Requests = append(store.Requests, r) 36 | store.Unlock() 37 | 38 | store.runCallbacks("requests") 39 | } 40 | 41 | func (store *Store) RemoveRequest(c string, approved bool) { 42 | store.Lock() 43 | for i, r := range store.Requests { 44 | if r.Connection == c { 45 | if r.Approved != nil { 46 | if approved { 47 | r.Approved <- true 48 | } 49 | close(r.Approved) 50 | r.Approved = nil 51 | } 52 | store.Requests = append(store.Requests[:i], store.Requests[i+1:]...) 53 | } 54 | } 55 | store.Unlock() 56 | 57 | store.runCallbacks("requests") 58 | } 59 | 60 | func (store *Store) AcceptRequest(c string) { 61 | store.RemoveRequest(c, true) 62 | } 63 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/wirepusher/wirepusher.go: -------------------------------------------------------------------------------- 1 | package wirepusher 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | type WirePusherSender struct { 11 | Title string `json:"title"` 12 | Type string `json:"type"` 13 | Action string `json:"action"` 14 | url string 15 | } 16 | 17 | func New(parameters json.RawMessage) *WirePusherSender { 18 | wp := &WirePusherSender{url: "https://wirepusher.com/send"} 19 | 20 | json.Unmarshal(parameters, wp) 21 | 22 | return wp 23 | } 24 | 25 | func (wp *WirePusherSender) Trigger(dest []string, body string) error { 26 | var failure error 27 | for _, d := range dest { 28 | err := wp.notify(true, d, body) 29 | if err != nil { 30 | failure = err 31 | } 32 | } 33 | 34 | return failure 35 | } 36 | 37 | func (wp *WirePusherSender) Release(dest []string, body string) error { 38 | var failure error 39 | for _, d := range dest { 40 | err := wp.notify(false, d, body) 41 | if err != nil { 42 | failure = err 43 | } 44 | } 45 | 46 | return failure 47 | } 48 | 49 | func (wp *WirePusherSender) notify(trigger bool, dest string, body string) error { 50 | u, err := url.Parse(wp.url) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | event := "Triggered" 56 | if !trigger { 57 | event = "Released" 58 | } 59 | 60 | q := u.Query() 61 | 62 | q.Set("id", dest) 63 | q.Set("title", fmt.Sprintf("%s - %s", wp.Title, event)) 64 | q.Set("message", body) 65 | q.Set("type", wp.Type) 66 | q.Set("action", wp.Action) 67 | 68 | u.RawQuery = q.Encode() 69 | 70 | req, err := http.NewRequest("GET", u.String(), nil) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | client := &http.Client{} 76 | resp, err := client.Do(req) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | defer resp.Body.Close() 82 | 83 | // b, err := ioutil.ReadAll(resp.Body) 84 | // spew.Dump(b) 85 | 86 | return err 87 | } 88 | 89 | func (wp *WirePusherSender) Destinations() (map[string]string, error) { 90 | return nil, fmt.Errorf("not implemented") 91 | } 92 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/routes/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import Card from '../../components/Card'; 5 | import Device from './Device'; 6 | 7 | class Dashboard extends Component { 8 | render() { 9 | const { devices, nodes } = this.props; 10 | 11 | const devicesByNode = devices.reduce((acc, device) => { 12 | const [node] = device.get('id').split('.', 2); 13 | if (acc[node] === undefined) { 14 | acc[node] = []; 15 | } 16 | acc[node].push(device); 17 | return acc; 18 | }, {}); 19 | 20 | return ( 21 | 22 |
25 | {Object.keys(devicesByNode).map(nodeId => ( 26 |
35 | 43 | {devicesByNode[nodeId] 44 | .sort((a, b) => a.get('name').localeCompare(b.get('name'))) 45 | .map(device => ( 46 | 50 | ))} 51 | 52 |
53 | ))} 54 |
55 |
56 | ); 57 | } 58 | } 59 | 60 | const mapToProps = state => ({ 61 | devices: state.getIn(['devices', 'list']), 62 | nodes: state.getIn(['nodes', 'list']), 63 | }); 64 | 65 | export default connect(mapToProps)(Dashboard); 66 | -------------------------------------------------------------------------------- /nodes/stampzilla-streamdeck/keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "strconv" 8 | 9 | "github.com/llgcode/draw2d" 10 | "github.com/llgcode/draw2d/draw2dimg" 11 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-streamdeck/streamdeck" 12 | ) 13 | 14 | func drawTempToKey(deck *streamdeck.StreamDeck, label string, value float32, key int) { 15 | dest := image.NewRGBA(image.Rect(0, 0, ICON_SIZE, ICON_SIZE)) 16 | gc := draw2dimg.NewGraphicContext(dest) 17 | 18 | gc.SetFillColor(color.RGBA{0xff, 0x66, 0x00, 0xff}) 19 | gc.SetStrokeColor(color.RGBA{0xff, 0xff, 0xff, 0xff}) 20 | 21 | gc.SetFontSize(28) 22 | gc.SetFontData(draw2d.FontData{ 23 | Name: "Roboto", 24 | }) 25 | 26 | text := fmt.Sprintf("%.0f", value) 27 | left, top, right, bottom := gc.GetStringBounds(text) 28 | gc.FillStringAt(text, 72/2-((right-left)/2), 72/2) 29 | 30 | // Label 31 | gc.SetFillColor(color.RGBA{0xff, 0xff, 0xff, 0xff}) 32 | gc.SetFontSize(14) 33 | left, top, right, bottom = gc.GetStringBounds(label) 34 | gc.FillStringAt(label, 72/2-((right-left)/2), 72-((bottom-top)/2)) 35 | 36 | deck.WriteImageToKey(dest, key) 37 | } 38 | 39 | func drawStateToKey(deck *streamdeck.StreamDeck, label string, value interface{}, key int) { 40 | dest := image.NewRGBA(image.Rect(0, 0, ICON_SIZE, ICON_SIZE)) 41 | gc := draw2dimg.NewGraphicContext(dest) 42 | 43 | text := "" 44 | switch state := value.(type) { 45 | case bool: 46 | text = strconv.FormatBool(state) 47 | case int: 48 | text = "int" 49 | default: 50 | text = "unkn" 51 | } 52 | 53 | // State 54 | gc.SetFillColor(color.RGBA{0xff, 0xff, 0xff, 0xff}) 55 | gc.SetStrokeColor(color.RGBA{0xff, 0xff, 0xff, 0xff}) 56 | 57 | gc.SetFontSize(28) 58 | gc.SetFontData(draw2d.FontData{ 59 | Name: "Roboto", 60 | }) 61 | 62 | left, top, right, bottom := gc.GetStringBounds(text) 63 | gc.FillStringAt(text, 72/2-((right-left)/2), 72/2) 64 | 65 | // Label 66 | gc.SetFillColor(color.RGBA{0xff, 0xff, 0xff, 0xff}) 67 | gc.SetFontSize(14) 68 | left, top, right, bottom = gc.GetStringBounds(label) 69 | gc.FillStringAt(label, 72/2-((right-left)/2), 72-((bottom-top)/2)) 70 | 71 | deck.WriteImageToKey(dest, key) 72 | } 73 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | class Login extends Component { 5 | static propTypes = {}; 6 | 7 | state = { 8 | username: '', 9 | password: '', 10 | }; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.autofocusRef = createRef(null); 15 | } 16 | 17 | componentDidMount() { 18 | if (this.autofocusRef.current) { 19 | this.autofocusRef.current.focus(); 20 | } 21 | } 22 | 23 | onChange = (field, value) => { 24 | this.setState({ 25 | [field]: value, 26 | }); 27 | }; 28 | 29 | onSubmit = (e) => { 30 | e.preventDefault(); 31 | 32 | this.props.onSubmit(this.state.username, this.state.password); 33 | }; 34 | 35 | render() { 36 | const { username, password } = this.state; 37 | const { error } = this.props; 38 | 39 | return ( 40 |
41 |
42 | 43 | this.onChange('username', e.target.value)} 48 | onFocus={(e) => e.currentTarget.select()} 49 | value={username} 50 | ref={this.autofocusRef} 51 | autoFocus 52 | /> 53 |
54 |
55 | 56 | this.onChange('password', e.target.value)} 61 | onFocus={(e) => e.currentTarget.select()} 62 | value={password} 63 | /> 64 | {error &&
{error.message}
} 65 |
66 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default Login; 75 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/components/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | class Register extends Component { 5 | static propTypes = {}; 6 | 7 | state = { 8 | username: '', 9 | password: '', 10 | }; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.autofocusRef = createRef(null); 15 | } 16 | 17 | componentDidMount() { 18 | if (this.autofocusRef.current) { 19 | this.autofocusRef.current.focus(); 20 | } 21 | } 22 | 23 | onChange = (field, value) => { 24 | this.setState({ 25 | [field]: value, 26 | }); 27 | }; 28 | 29 | onSubmit = (e) => { 30 | e.preventDefault(); 31 | 32 | this.props.onSubmit(this.state.username, this.state.password); 33 | }; 34 | 35 | render() { 36 | const { username, password } = this.state; 37 | const { error } = this.props; 38 | 39 | return ( 40 |
41 |
42 | 43 | this.onChange('username', e.target.value)} 48 | onFocus={(e) => e.currentTarget.select()} 49 | value={username} 50 | ref={this.autofocusRef} 51 | autoFocus 52 | /> 53 |
54 |
55 | 56 | this.onChange('password', e.target.value)} 61 | onFocus={(e) => e.currentTarget.select()} 62 | value={password} 63 | /> 64 | {error &&
{error.message}
} 65 |
66 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default Register; 75 | -------------------------------------------------------------------------------- /nodes/stampzilla-google-assistant/oauthhandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/RangelReale/osin" 8 | "github.com/gin-gonic/gin" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func authorize(oauth2server *osin.Server) func(c *gin.Context) { 13 | return func(c *gin.Context) { 14 | resp := oauth2server.NewResponse() 15 | defer resp.Close() 16 | 17 | if ar := oauth2server.HandleAuthorizeRequest(resp, c.Request); ar != nil { 18 | // HANDLE LOGIN PAGE HERE 19 | if !handleLoginPage(ar, c.Writer, c.Request) { 20 | return 21 | } 22 | // ar.UserData = struct{ Login string }{Login: "test"} 23 | ar.Authorized = true 24 | oauth2server.FinishAuthorizeRequest(resp, c.Request, ar) 25 | } 26 | if resp.IsError && resp.InternalError != nil { 27 | logrus.Errorf("ERROR: %#v\n", resp.InternalError) 28 | } 29 | osin.OutputJSON(resp, c.Writer, c.Request) 30 | } 31 | } 32 | 33 | func token(oauth2server *osin.Server) func(c *gin.Context) { 34 | return func(c *gin.Context) { 35 | resp := oauth2server.NewResponse() 36 | defer resp.Close() 37 | 38 | if ar := oauth2server.HandleAccessRequest(resp, c.Request); ar != nil { 39 | ar.Authorized = true 40 | oauth2server.FinishAccessRequest(resp, c.Request, ar) 41 | } 42 | if resp.IsError && resp.InternalError != nil { 43 | logrus.Errorf("ERROR: %#v\n", resp.InternalError) 44 | } 45 | osin.OutputJSON(resp, c.Writer, c.Request) 46 | } 47 | } 48 | 49 | func handleLoginPage(ar *osin.AuthorizeRequest, w http.ResponseWriter, r *http.Request) bool { 50 | r.ParseForm() 51 | if r.Method == "POST" && r.Form.Get("login") == "test" && r.Form.Get("password") == "test" { 52 | return true 53 | } 54 | 55 | w.Write([]byte("")) 56 | 57 | w.Write([]byte(fmt.Sprintf("LOGIN %s (use test/test)
", ar.Client.GetId()))) 58 | w.Write([]byte(fmt.Sprintf("
", r.URL.RawQuery))) 59 | 60 | w.Write([]byte("Login:
")) 61 | w.Write([]byte("Password:
")) 62 | w.Write([]byte("")) 63 | 64 | w.Write([]byte("
")) 65 | 66 | w.Write([]byte("")) 67 | 68 | return false 69 | } 70 | -------------------------------------------------------------------------------- /nodes/stampzilla-google-assistant/googleassistant/request.go: -------------------------------------------------------------------------------- 1 | package googleassistant 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // Intent of the action. 10 | type Intent string 11 | 12 | const ( 13 | // SyncIntent is used when syncing devices. 14 | SyncIntent = "action.devices.SYNC" 15 | // QueryIntent is used when querying for status. 16 | QueryIntent = "action.devices.QUERY" 17 | // ExecuteIntent is used when controlling devices. 18 | ExecuteIntent = "action.devices.EXECUTE" 19 | 20 | CommandBrightnessAbsolute = "action.devices.commands.BrightnessAbsolute" 21 | CommandOnOff = "action.devices.commands.OnOff" 22 | ) 23 | 24 | // Inputs from google. 25 | type Inputs []map[string]json.RawMessage 26 | 27 | func (i Inputs) Intent() Intent { 28 | for _, v := range i { 29 | if v, ok := v["intent"]; ok { 30 | in := "" 31 | err := json.Unmarshal(v, &in) 32 | if err != nil { 33 | logrus.Error(err) 34 | return "" 35 | } 36 | 37 | return Intent(in) 38 | } 39 | } 40 | return "" 41 | } 42 | 43 | func (i Inputs) Payload() Payload { 44 | for _, v := range i { 45 | if v, ok := v["payload"]; ok { 46 | pl := Payload{} 47 | err := json.Unmarshal(v, &pl) 48 | if err != nil { 49 | logrus.Error(err) 50 | return Payload{} 51 | } 52 | return pl 53 | } 54 | } 55 | return Payload{} 56 | } 57 | 58 | type Payload struct { 59 | Commands []struct { 60 | Devices []struct { 61 | ID string `json:"id"` 62 | // CustomData struct { 63 | // FooValue int `json:"fooValue"` 64 | // BarValue bool `json:"barValue"` 65 | // BazValue string `json:"bazValue"` 66 | // } `json:"customData"` 67 | } `json:"devices"` 68 | Execution []struct { 69 | Command string `json:"command"` 70 | Params struct { 71 | On bool `json:"on"` 72 | Brightness int `json:"brightness"` 73 | } `json:"params"` 74 | } `json:"execution"` 75 | } `json:"commands"` 76 | Devices []struct { 77 | ID string `json:"id"` 78 | // CustomData struct { 79 | // FooValue int `json:"fooValue"` 80 | // BarValue bool `json:"barValue"` 81 | // BazValue string `json:"bazValue"` 82 | // } `json:"customData"` 83 | } `json:"devices"` 84 | } 85 | 86 | type Request struct { 87 | RequestID string 88 | Inputs Inputs 89 | } 90 | -------------------------------------------------------------------------------- /nodes/stampzilla-knx/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/vapourismo/knx-go/knx" 8 | "github.com/vapourismo/knx-go/knx/cemi" 9 | "github.com/vapourismo/knx-go/knx/dpt" 10 | ) 11 | 12 | type config struct { 13 | Gateway gateway `json:"gateway"` 14 | Lights lights `json:"lights"` 15 | Sensors []sensor `json:"sensors"` 16 | sync.Mutex 17 | } 18 | 19 | func (c *config) GetLight(id string) *light { 20 | c.Lock() 21 | defer c.Unlock() 22 | for _, v := range c.Lights { 23 | if v.ID == id { 24 | return &v 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | type gateway struct { 31 | Address string `json:"address"` 32 | } 33 | 34 | type lights []light 35 | 36 | type light struct { 37 | ID string `json:"id"` 38 | ControlSwitch string `json:"control_switch"` 39 | ControlBrightness string `json:"control_brightness"` 40 | StateSwitch string `json:"state_switch"` 41 | StateBrightness string `json:"state_brightness"` 42 | } 43 | 44 | type sensor struct { 45 | ID string `json:"id"` 46 | Motion string `json:"motion"` 47 | MotionTrue string `json:"motionTrue"` 48 | Lux string `json:"lux"` 49 | Temperature string `json:"temperature"` 50 | Humidity string `json:"humidity"` 51 | Co2 string `json:"co2"` 52 | Voc string `json:"voc"` 53 | AirPressure string `json:"airPressure"` 54 | DewPoint string `json:"dewPoint"` 55 | } 56 | 57 | func (light *light) Switch(tunnel *tunnel, target bool) error { 58 | if !tunnel.Connected() { 59 | return fmt.Errorf("not connected to KNX gateway") 60 | } 61 | addr, err := cemi.NewGroupAddrString(light.ControlSwitch) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | cmd := knx.GroupEvent{ 67 | Command: knx.GroupWrite, 68 | Destination: addr, 69 | Data: dpt.DPT_1001(target).Pack(), 70 | } 71 | return tunnel.Send(cmd) 72 | } 73 | 74 | func (light *light) Brightness(tunnel *tunnel, target float64) error { 75 | if !tunnel.Connected() { 76 | return fmt.Errorf("not connected to KNX gateway") 77 | } 78 | addr, err := cemi.NewGroupAddrString(light.ControlBrightness) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | cmd := knx.GroupEvent{ 84 | Command: knx.GroupWrite, 85 | Destination: addr, 86 | Data: dpt.DPT_5001(float32(target)).Pack(), 87 | } 88 | return tunnel.Send(cmd) 89 | } 90 | -------------------------------------------------------------------------------- /nodes/stampzilla-nx-witness/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type API struct { 14 | config *Config 15 | } 16 | 17 | func NewAPI(config *Config) *API { 18 | return &API{ 19 | config: config, 20 | } 21 | } 22 | 23 | func (api *API) fetchEventRules() (EventRulesResponse, error) { 24 | url := fmt.Sprintf("%s/ec2/getEventRules", api.config.Host) 25 | req, err := http.NewRequest("GET", url, nil) 26 | if err != nil { 27 | return nil, fmt.Errorf("fetchEventRules: error create request: %w", err) 28 | } 29 | 30 | req.Header.Set("Content-Type", "application/json") 31 | req.SetBasicAuth(api.config.Username, api.config.Password) 32 | 33 | resp, err := httpClient.Do(req) 34 | if err != nil { 35 | return nil, err 36 | } 37 | defer resp.Body.Close() 38 | 39 | if resp.StatusCode != 200 { 40 | return nil, fmt.Errorf("error fetching from api status: %d", resp.StatusCode) 41 | } 42 | 43 | response := EventRulesResponse{} 44 | err = json.NewDecoder(resp.Body).Decode(&response) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return response, nil 49 | } 50 | 51 | func (api *API) saveEventRule(rule Rule) error { 52 | url := fmt.Sprintf("%s/ec2/saveEventRule", api.config.Host) 53 | data, err := json.Marshal(&rule) 54 | if err != nil { 55 | return fmt.Errorf("saveEventRule: error marshal json: %w", err) 56 | } 57 | 58 | req, err := http.NewRequest("POST", url, bytes.NewReader(data)) 59 | if err != nil { 60 | return fmt.Errorf("saveEventRule: error creating request: %w", err) 61 | } 62 | 63 | req.Header.Set("Content-Type", "application/json") 64 | req.SetBasicAuth(api.config.Username, api.config.Password) 65 | 66 | logrus.Debugf("req to url: %s", url) 67 | 68 | resp, err := httpClient.Do(req) 69 | if err != nil { 70 | return fmt.Errorf("saveEventRule: error making request: %w", err) 71 | } 72 | 73 | defer resp.Body.Close() 74 | b, err := ioutil.ReadAll(resp.Body) 75 | if err != nil { 76 | return fmt.Errorf("saveEventRule: error reading body: %w", err) 77 | } 78 | 79 | logrus.Debugf("statusCode: %s", resp.Status) 80 | logrus.Debugf("body: %s", b) 81 | 82 | if resp.StatusCode != 200 { 83 | return fmt.Errorf("error saving to api, status: %d, body: %s", resp.StatusCode, string(b)) 84 | } 85 | 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /nodes/stampzilla-google-assistant/googleassistant/response.go: -------------------------------------------------------------------------------- 1 | package googleassistant 2 | 3 | type DeviceName struct { 4 | DefaultNames []string `json:"defaultNames,omitempty"` 5 | Name string `json:"name,omitempty"` 6 | Nicknames []string `json:"nicknames,omitempty"` 7 | } 8 | 9 | type DeviceAttributes struct { 10 | ColorModel string `json:"colorModel,omitempty"` 11 | TemperatureMinK int `json:"temperatureMinK,omitempty"` 12 | TemperatureMaxK int `json:"temperatureMaxK,omitempty"` 13 | } 14 | 15 | type Device struct { 16 | ID string `json:"id"` 17 | Type string `json:"type"` 18 | Traits []string `json:"traits"` 19 | Name DeviceName `json:"name"` 20 | WillReportState bool `json:"willReportState"` 21 | // DeviceInfo struct { 22 | // Manufacturer string `json:"manufacturer"` 23 | // Model string `json:"model"` 24 | // HwVersion string `json:"hwVersion"` 25 | // SwVersion string `json:"swVersion"` 26 | // } `json:"deviceInfo"` 27 | // CustomData struct { 28 | // FooValue int `json:"fooValue"` 29 | // BarValue bool `json:"barValue"` 30 | // BazValue string `json:"bazValue"` 31 | // } `json:"customData"` 32 | Attributes DeviceAttributes `json:"attributes,omitempty"` 33 | } 34 | 35 | type ResponseStates struct { 36 | On bool `json:"on,omitempty"` 37 | Brightness int `json:"brightness,omitempty"` 38 | Online bool `json:"online,omitempty"` 39 | } 40 | 41 | func NewResponseCommand() ResponseCommand { 42 | return ResponseCommand{ 43 | States: ResponseStates{}, 44 | Status: "SUCCESS", 45 | } 46 | } 47 | 48 | type ResponseCommand struct { 49 | IDs []string `json:"ids"` 50 | Status string `json:"status"` 51 | States ResponseStates `json:"states"` 52 | ErrorCode string `json:"errorCode,omitempty"` 53 | } 54 | 55 | type Response struct { 56 | RequestID string `json:"requestId"` 57 | Payload struct { 58 | AgentUserID string `json:"agentUserId,omitempty"` 59 | Devices []Device `json:"devices,omitempty"` 60 | Commands []ResponseCommand `json:"commands,omitempty"` 61 | } `json:"payload"` 62 | } 63 | 64 | type QueryResponse struct { 65 | RequestID string `json:"requestId"` 66 | Payload struct { 67 | Devices map[string]map[string]interface{} `json:"devices,omitempty"` 68 | } `json:"payload"` 69 | } 70 | -------------------------------------------------------------------------------- /nodes/stampzilla-mbus/worker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os" 10 | "sync" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/jonaz/gombus" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | type Worker struct { 19 | wg sync.WaitGroup 20 | work chan func(*gombus.Conn) error 21 | reconnect chan struct{} 22 | cancel context.CancelFunc 23 | conn *gombus.Conn 24 | config *Config 25 | } 26 | 27 | func NewWorker(config *Config) *Worker { 28 | return &Worker{ 29 | work: make(chan func(*gombus.Conn) error), 30 | reconnect: make(chan struct{}), 31 | config: config, 32 | } 33 | } 34 | 35 | func (w *Worker) Do(fn func(*gombus.Conn) error) { 36 | w.work <- fn 37 | } 38 | 39 | func (w *Worker) Start(parentCtx context.Context, workers int) { 40 | var ctx context.Context 41 | ctx, w.cancel = context.WithCancel(parentCtx) 42 | 43 | go func() { 44 | defer w.wg.Done() 45 | for { 46 | select { 47 | case <-ctx.Done(): 48 | return 49 | case <-w.reconnect: 50 | for { 51 | err := w.connectTCP() 52 | if err != nil { 53 | logrus.Error(err) 54 | time.Sleep(time.Second * 1) 55 | continue 56 | } 57 | break 58 | } 59 | } 60 | } 61 | }() 62 | w.reconnect <- struct{}{} 63 | for i := 0; i < workers; i++ { 64 | w.wg.Add(1) 65 | go w.start(ctx) 66 | } 67 | } 68 | 69 | func (w *Worker) connectTCP() error { 70 | var err error 71 | w.conn, err = gombus.Dial(net.JoinHostPort(w.config.Host, w.config.Port)) 72 | if err != nil { 73 | return fmt.Errorf("error connecting to mbus: %w", err) 74 | } 75 | 76 | if conn, ok := w.conn.Conn().(*net.TCPConn); ok { 77 | er := conn.SetKeepAlive(true) 78 | if er != nil { 79 | return er 80 | } 81 | 82 | er = conn.SetKeepAlivePeriod(time.Second * 10) 83 | if er != nil { 84 | return er 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (w *Worker) start(ctx context.Context) { 92 | defer w.wg.Done() 93 | 94 | for { 95 | select { 96 | case <-ctx.Done(): 97 | return 98 | case fn := <-w.work: 99 | err := fn(w.conn) 100 | if err != nil { 101 | logrus.Error(err) 102 | } 103 | 104 | if os.IsTimeout(err) || errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) { 105 | w.reconnect <- struct{}{} 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /nodes/stampzilla-husdata-h60/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/e2e" 12 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestUpdateState(t *testing.T) { 17 | main, _, cleanup := e2e.SetupWebsocketTest(t) 18 | defer cleanup() 19 | e2e.AcceptCertificateRequest(t, main) 20 | 21 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | // Assert we called our heatpump with the correct parameters 23 | if r.URL.Query().Get("idx") == "0203" { 24 | assert.Equal(t, "/api/set?idx=0203&val=225", r.URL.String()) 25 | return 26 | } 27 | if r.URL.Query().Get("idx") == "6209" { 28 | assert.Equal(t, "/api/set?idx=6209&val=2", r.URL.String()) 29 | return 30 | } 31 | t.Error("unexpected get parameters") 32 | })) 33 | defer ts.Close() 34 | 35 | config := NewConfig() 36 | config.Host = ts.URL 37 | node := setupNode(config) 38 | 39 | err := node.Connect() 40 | assert.NoError(t, err) 41 | 42 | dev := &devices.Device{ 43 | Name: "heatpump", 44 | Type: "sensor", 45 | ID: devices.ID{ID: "1"}, 46 | Online: true, 47 | Traits: []string{"TemperatureControl"}, 48 | State: make(devices.State), 49 | } 50 | node.AddOrUpdate(dev) 51 | 52 | b := []byte(fmt.Sprintf(` 53 | { 54 | "type": "state-change", 55 | "body": { 56 | "%s.1": { 57 | "type": "light", 58 | "id": "%s.1", 59 | "name": "heatpump", 60 | "online": true, 61 | "state": { 62 | "RoomTempSetpoint": 22.5, 63 | "ExtraWarmWater": 2 64 | } 65 | } 66 | } 67 | } 68 | `, node.UUID, node.UUID)) 69 | 70 | err = node.Client.WriteMessage(websocket.TextMessage, b) 71 | assert.NoError(t, err) 72 | var syncedDev *devices.Device 73 | e2e.WaitFor(t, 1*time.Second, "wait for node to have updated RoomTempSetpoint", func() bool { 74 | syncedDev = node.GetDevice("1") 75 | if dev != nil && syncedDev.State["RoomTempSetpoint"] != nil { 76 | return true 77 | } 78 | return false 79 | }) 80 | assert.Equal(t, 22.5, syncedDev.State["RoomTempSetpoint"]) 81 | assert.Equal(t, float64(2), syncedDev.State["ExtraWarmWater"]) 82 | } 83 | -------------------------------------------------------------------------------- /nodes/stampzilla-linux/dpms.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | "github.com/sirupsen/logrus" 14 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 15 | ) 16 | 17 | func startMonitorDpms() { 18 | c1 := exec.Command("ls", "/tmp/.X11-unix") 19 | c2 := exec.Command("tr", "X", ":") 20 | r, w := io.Pipe() 21 | c1.Stdout = w 22 | c2.Stdin = r 23 | 24 | var b2 bytes.Buffer 25 | c2.Stdout = &b2 26 | 27 | c1.Start() 28 | c2.Start() 29 | c1.Wait() 30 | w.Close() 31 | c2.Wait() 32 | result := strings.Split(strings.TrimSpace(b2.String()), "\n") 33 | 34 | for _, screen := range result { 35 | go monitorDpms(screen) 36 | } 37 | } 38 | 39 | func monitorDpms(screen string) { 40 | dev := &devices.Device{ 41 | Name: "Monitor " + screen, 42 | ID: devices.ID{ID: "monitor" + screen}, 43 | Online: true, 44 | Traits: []string{"OnOff"}, 45 | State: devices.State{ 46 | "on": false, 47 | }, 48 | } 49 | added := false 50 | 51 | re := regexp.MustCompile("Monitor is (in )?([^ \n]+)") 52 | 53 | for { 54 | cmd := exec.Command("xset", "q") 55 | cmd.Env = append(os.Environ(), "DISPLAY="+screen) 56 | var stderr bytes.Buffer 57 | cmd.Stderr = &stderr 58 | out, err := cmd.Output() 59 | if err != nil { 60 | logrus.Errorf("Failed to read monitor status: %s: %s", fmt.Sprint(err), stderr.String()) 61 | return 62 | } 63 | 64 | if !added { 65 | n.AddOrUpdate(dev) 66 | added = true 67 | } 68 | 69 | status := re.FindStringSubmatch(string(out)) 70 | if len(status) > 2 { 71 | newState := make(devices.State) 72 | newState["monitor_status"] = status[2] 73 | newState["on"] = status[2] == "On" 74 | n.UpdateState(dev.ID.ID, newState) 75 | } 76 | <-time.After(time.Second * 1) 77 | } 78 | } 79 | 80 | func changeDpmsState(screen string, state bool) error { 81 | if state { 82 | cmd := exec.Command("xset", "dpms", "force", "on") 83 | cmd.Env = append(os.Environ(), "DISPLAY="+screen) 84 | _, err := cmd.Output() 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | if !state { 90 | cmd := exec.Command("xset", "dpms", "force", "off") 91 | cmd.Env = append(os.Environ(), "DISPLAY="+screen) 92 | _, err := cmd.Output() 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/destination_test.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestEqual(t *testing.T) { 13 | d1 := &Destination{ 14 | UUID: "uuid1", 15 | Type: "type1", 16 | Name: "name1", 17 | Sender: "sender1", 18 | Destinations: []string{ 19 | "a", "b", 20 | }, 21 | } 22 | d2 := &Destination{ 23 | UUID: "uuid1", 24 | Type: "type1", 25 | Name: "name1", 26 | Sender: "sender1", 27 | Destinations: []string{ 28 | "a", "b", 29 | }, 30 | } 31 | assert.True(t, d1.Equal(d2)) 32 | 33 | d3 := &Destination{ 34 | UUID: "uuid1", 35 | Type: "type1", 36 | Name: "name1", 37 | Sender: "sender2", 38 | } 39 | assert.False(t, d1.Equal(d3)) 40 | 41 | d3.Sender = d1.Sender 42 | d3.Name = "name3" 43 | 44 | assert.False(t, d1.Equal(d3)) 45 | 46 | d3.Name = d1.Name 47 | d3.Type = "type3" 48 | 49 | assert.False(t, d1.Equal(d3)) 50 | 51 | d3.Type = d1.Type 52 | d3.UUID = "uuid3" 53 | 54 | assert.False(t, d1.Equal(d3)) 55 | 56 | d3.UUID = d1.UUID 57 | d3.Destinations = []string{ 58 | "a", 59 | } 60 | 61 | assert.False(t, d1.Equal(d3)) 62 | 63 | d3.Destinations = []string{ 64 | "a", "c", 65 | } 66 | 67 | assert.False(t, d1.Equal(d3)) 68 | } 69 | 70 | func TestAddDestination(t *testing.T) { 71 | dests := NewDestinations() 72 | d1 := &Destination{ 73 | Name: "name", 74 | UUID: "uuid", 75 | } 76 | dests.Add(d1) 77 | assert.Equal(t, "name", dests.Get("uuid").Name) 78 | } 79 | 80 | func TestRemoveDestination(t *testing.T) { 81 | dests := NewDestinations() 82 | d1 := &Destination{ 83 | Name: "name", 84 | UUID: "uuid", 85 | } 86 | dests.Add(d1) 87 | dests.Remove("uuid") 88 | assert.Len(t, dests.All(), 0) 89 | } 90 | 91 | func TestReadWrite(t *testing.T) { 92 | file, err := ioutil.TempFile("", "readwritedestinations") 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | defer os.Remove(file.Name()) 97 | 98 | ds1 := NewDestinations() 99 | ds2 := NewDestinations() 100 | 101 | d1 := &Destination{ 102 | Name: "name", 103 | UUID: "uuid", 104 | } 105 | ds1.Add(d1) 106 | 107 | err = ds1.Save(file.Name()) 108 | assert.NoError(t, err) 109 | 110 | err = ds2.Load(file.Name()) 111 | assert.NoError(t, err) 112 | 113 | d2 := ds2.Get("uuid") 114 | 115 | assert.True(t, d1.Equal(d2)) 116 | } 117 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/persons/person.go: -------------------------------------------------------------------------------- 1 | package persons 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 8 | "golang.org/x/crypto/bcrypt" 9 | ) 10 | 11 | type Person struct { 12 | UUID string `json:"uuid"` 13 | Name string `json:"name"` 14 | Username string `json:"username"` 15 | Email string `json:"email"` 16 | AllowLogin bool `json:"allow_login"` 17 | IsAdmin bool `json:"is_admin"` 18 | 19 | LastSeen time.Time `json:"last_seen"` 20 | 21 | State devices.State `json:"state"` 22 | } 23 | 24 | type PersonWithPassword struct { 25 | Person 26 | Password string `json:"password"` 27 | } 28 | 29 | type PersonWithPasswords struct { 30 | PersonWithPassword 31 | NewPassword string `json:"new_password,omitempty"` 32 | RepeatPassword string `json:"repeat_password,omitempty"` 33 | } 34 | 35 | // Equal checks if 2 persons are equal. 36 | func (a Person) Equal(b PersonWithPasswords) bool { 37 | if !a.State.Equal(b.State) { // this is most likely to not be equal so we check it first 38 | return false 39 | } 40 | if a.Name != b.Name { 41 | return false 42 | } 43 | if a.Username != b.Username { 44 | return false 45 | } 46 | if a.Email != b.Email { 47 | return false 48 | } 49 | if b.Password != "" { 50 | return false 51 | } 52 | if a.LastSeen != b.LastSeen { 53 | return false 54 | } 55 | if a.AllowLogin != b.AllowLogin { 56 | return false 57 | } 58 | if a.IsAdmin != b.IsAdmin { 59 | return false 60 | } 61 | if b.NewPassword != "" { 62 | return false 63 | } 64 | 65 | return true 66 | } 67 | 68 | func (a *PersonWithPasswords) UpdatePassword() error { 69 | if a.NewPassword != "" && a.NewPassword != a.RepeatPassword { 70 | return fmt.Errorf("repeat password does not match the new password") 71 | } 72 | 73 | // Change password 74 | if a.NewPassword != "" && a.NewPassword == a.RepeatPassword { 75 | hash, err := bcrypt.GenerateFromPassword([]byte(a.NewPassword), bcrypt.DefaultCost) 76 | if err != nil { 77 | return fmt.Errorf("failed to generate password hash: %s", err) 78 | } 79 | 80 | a.Password = string(hash) 81 | } 82 | 83 | a.NewPassword = "" 84 | a.RepeatPassword = "" 85 | 86 | return nil 87 | } 88 | 89 | func (p *PersonWithPassword) CheckPassword(password string) error { 90 | if err := bcrypt.CompareHashAndPassword([]byte(p.Password), []byte(password)); err != nil { 91 | return fmt.Errorf("wrong username or password") 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/devices/state.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | type State map[string]interface{} 4 | 5 | func (ds State) Clone() State { 6 | newState := make(State) 7 | for k, v := range ds { 8 | newState[k] = v 9 | } 10 | return newState 11 | } 12 | 13 | // Bool runs fn only if key is found in map and it is of type bool. 14 | func (ds State) Bool(key string, fn func(bool)) { 15 | if v, ok := ds[key]; ok { 16 | if v, ok := v.(bool); ok { 17 | fn(v) 18 | } 19 | } 20 | } 21 | 22 | // Int runs fn only if key is found in map and it is of type int. 23 | func (ds State) Int(key string, fn func(int64)) { 24 | if v, ok := ds[key]; ok { 25 | if v, ok := v.(int); ok { 26 | fn(int64(v)) 27 | } 28 | if v, ok := v.(int64); ok { 29 | fn(v) 30 | } 31 | } 32 | } 33 | 34 | // Float runs fn only if key is found in map and it is of type float64. 35 | func (ds State) Float(key string, fn func(float64)) { 36 | if v, ok := ds[key]; ok { 37 | if v, ok := v.(float64); ok { 38 | fn(v) 39 | } 40 | } 41 | } 42 | 43 | // String runs fn only if key is found in map and it is of type string. 44 | func (ds State) String(key string, fn func(string)) { 45 | if v, ok := ds[key]; ok { 46 | if v, ok := v.(string); ok { 47 | fn(v) 48 | } 49 | } 50 | } 51 | 52 | // Diff calculates the diff between 2 states. If key is missing in right but exists in left it will not be a diff. 53 | func (ds State) Diff(right State) State { 54 | diff := make(State) 55 | for k, v := range ds { 56 | rv, ok := right[k] 57 | if !ok { 58 | // diff[k] = v 59 | continue 60 | } 61 | if ok && v != rv { 62 | diff[k] = rv 63 | } 64 | } 65 | 66 | for k, v := range right { 67 | if _, ok := ds[k]; !ok { 68 | diff[k] = v 69 | } 70 | } 71 | 72 | return diff 73 | } 74 | 75 | // Merge two states. 76 | func (ds State) Merge(right State) State { 77 | diff := make(State) 78 | for k, v := range ds { 79 | diff[k] = v 80 | } 81 | for k, v := range right { 82 | diff[k] = v 83 | } 84 | return diff 85 | } 86 | 87 | func (ds State) MergeWith(right State) { 88 | for k, v := range right { 89 | ds[k] = v 90 | } 91 | } 92 | 93 | func (ds State) Equal(right State) bool { 94 | if (ds == nil) != (right == nil) { 95 | return false 96 | } 97 | 98 | if len(ds) != len(right) { 99 | return false 100 | } 101 | for k := range ds { 102 | if ds[k] != right[k] { 103 | return false 104 | } 105 | } 106 | return true 107 | } 108 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/models/notification/file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestTrigger(t *testing.T) { 14 | f := New(json.RawMessage("{\"append\": false, \"timestamp\": false}")) 15 | 16 | file, err := ioutil.TempFile("", "testTrigger") 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | defer os.Remove(file.Name()) 21 | 22 | err = f.Trigger([]string{file.Name()}, "test1") 23 | assert.NoError(t, err) 24 | 25 | err = f.Trigger([]string{file.Name()}, "test2") 26 | assert.NoError(t, err) 27 | 28 | h, err := os.Open(file.Name()) 29 | assert.NoError(t, err) 30 | defer h.Close() 31 | 32 | b, err := ioutil.ReadAll(h) 33 | 34 | assert.Equal(t, b, []byte("test2\tTriggered\r\n")) 35 | } 36 | 37 | func TestTriggerAppend(t *testing.T) { 38 | f := New(json.RawMessage("{\"append\": true, \"timestamp\": false}")) 39 | 40 | file, err := ioutil.TempFile("", "testTrigger") 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | defer os.Remove(file.Name()) 45 | 46 | err = f.Trigger([]string{file.Name()}, "test1") 47 | assert.NoError(t, err) 48 | 49 | err = f.Release([]string{file.Name()}, "test2") 50 | assert.NoError(t, err) 51 | 52 | h, err := os.Open(file.Name()) 53 | assert.NoError(t, err) 54 | defer h.Close() 55 | 56 | b, err := ioutil.ReadAll(h) 57 | 58 | assert.Equal(t, b, []byte("test1\tTriggered\r\ntest2\tReleased\r\n")) 59 | } 60 | 61 | func TestTriggerTimestamp(t *testing.T) { 62 | f := New(json.RawMessage("{\"append\": false, \"timestamp\": true}")) 63 | 64 | file, err := ioutil.TempFile("", "testTrigger") 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | defer os.Remove(file.Name()) 69 | 70 | err = f.Trigger([]string{file.Name()}, "test1") 71 | assert.NoError(t, err) 72 | 73 | h, err := os.Open(file.Name()) 74 | assert.NoError(t, err) 75 | defer h.Close() 76 | 77 | b, err := ioutil.ReadAll(h) 78 | 79 | assert.Equal(t, len(b), 37) 80 | } 81 | 82 | func TestNoFile(t *testing.T) { 83 | f := New(json.RawMessage("")) 84 | 85 | err := f.Trigger([]string{"/"}, "test1") 86 | assert.Error(t, err) 87 | 88 | err = f.Release([]string{"/"}, "test1") 89 | assert.Error(t, err) 90 | } 91 | 92 | func TestDestinations(t *testing.T) { 93 | f := New(json.RawMessage("")) 94 | 95 | d, err := f.Destinations() 96 | 97 | assert.Error(t, err) 98 | assert.Nil(t, d) 99 | } 100 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/logic/savedstate.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sync" 8 | 9 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 10 | ) 11 | 12 | /* savedstate.json example 13 | { 14 | "6fbaea24-6b3f-4856-9194-735b349bbf4d": { 15 | "name": "test state 1", 16 | "uuid": "6fbaea24-6b3f-4856-9194-735b349bbf4d", 17 | "state": { 18 | "nodeuuid.deviceid": { 19 | "on": true 20 | } 21 | } 22 | } 23 | } 24 | */ 25 | 26 | type SavedStates map[string]*SavedState 27 | 28 | type SavedState struct { 29 | Name string `json:"name"` 30 | UUID string `json:"uuid"` 31 | State map[devices.ID]devices.State `json:"state"` 32 | } 33 | 34 | type SavedStateStore struct { 35 | State SavedStates 36 | sync.RWMutex 37 | } 38 | 39 | func (sss *SavedStateStore) Get(id string) *SavedState { 40 | sss.RLock() 41 | defer sss.RUnlock() 42 | return sss.State[id] 43 | } 44 | 45 | func (sss *SavedStateStore) All() SavedStates { 46 | sss.RLock() 47 | defer sss.RUnlock() 48 | return sss.State 49 | } 50 | 51 | func NewSavedStateStore() *SavedStateStore { 52 | return &SavedStateStore{ 53 | State: make(map[string]*SavedState), 54 | } 55 | } 56 | 57 | func (sss *SavedStateStore) SetState(s SavedStates) { 58 | sss.Lock() 59 | sss.State = s 60 | sss.Unlock() 61 | } 62 | 63 | func (sss *SavedStateStore) Save() error { 64 | sss.Lock() 65 | defer sss.Unlock() 66 | configFile, err := os.Create("savedstate.json") 67 | if err != nil { 68 | return fmt.Errorf("savedstate: error saving savedstate.json: %s", err.Error()) 69 | } 70 | encoder := json.NewEncoder(configFile) 71 | encoder.SetIndent("", "\t") 72 | err = encoder.Encode(sss.State) 73 | if err != nil { 74 | return fmt.Errorf("savedstate: error saving savedstate.json: %s", err.Error()) 75 | } 76 | return nil 77 | } 78 | 79 | func (sss *SavedStateStore) Load() error { 80 | configFile, err := os.Open("savedstate.json") 81 | if err != nil { 82 | if os.IsNotExist(err) { 83 | return nil // We dont want to error our if the file does not exist when we start the server 84 | } 85 | return fmt.Errorf("savedstate: error loading savedstate.json: %s", err.Error()) 86 | } 87 | 88 | sss.Lock() 89 | defer sss.Unlock() 90 | jsonParser := json.NewDecoder(configFile) 91 | if err = jsonParser.Decode(&sss.State); err != nil { 92 | return fmt.Errorf("savedstate: error loading savedstate.json: %s", err.Error()) 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /nodes/stampzilla-knx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices" 11 | "github.com/stampzilla/stampzilla-go/v2/pkg/node" 12 | ) 13 | 14 | func main() { 15 | node := node.New("knx") 16 | 17 | tunnel := newTunnel(node) 18 | tunnel.OnConnect = func() { 19 | for _, dev := range node.Devices.All() { 20 | dev.SetOnline(true) 21 | } 22 | node.SyncDevices() 23 | } 24 | tunnel.OnDisconnect = func() { 25 | for _, dev := range node.Devices.All() { 26 | dev.SetOnline(false) 27 | } 28 | node.SyncDevices() 29 | } 30 | 31 | config := &config{} 32 | 33 | node.OnConfig(updatedConfig(node, tunnel, config)) 34 | node.OnRequestStateChange(func(state devices.State, device *devices.Device) error { 35 | id := strings.SplitN(device.ID.ID, ".", 2) 36 | 37 | switch id[0] { 38 | case "light": 39 | 40 | light := config.GetLight(id[1]) 41 | state.Bool("on", func(on bool) { 42 | err := light.Switch(tunnel, on) 43 | if err != nil { 44 | logrus.Error() 45 | } 46 | }) 47 | state.Float("brightness", func(v float64) { 48 | err := light.Brightness(tunnel, v*100) 49 | if err != nil { 50 | logrus.Error() 51 | } 52 | }) 53 | 54 | default: 55 | return fmt.Errorf("Unknown device type \"%s\"", id[0]) 56 | } 57 | return nil 58 | }) 59 | 60 | ctx, shutdown := context.WithCancel(context.Background()) 61 | node.OnShutdown(func() { 62 | shutdown() 63 | }) 64 | 65 | err := node.Connect() 66 | if err != nil { 67 | logrus.Error(err) 68 | return 69 | } 70 | 71 | tunnel.Start(ctx) 72 | 73 | logrus.SetFormatter(&logrus.TextFormatter{ 74 | ForceColors: true, 75 | }) 76 | logrus.SetReportCaller(false) 77 | 78 | tunnel.Wait() 79 | node.Wait() 80 | } 81 | 82 | func updatedConfig(node *node.Node, tunnel *tunnel, config *config) func(data json.RawMessage) error { 83 | return func(data json.RawMessage) error { 84 | config.Lock() 85 | defer config.Unlock() 86 | err := json.Unmarshal(data, config) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | tunnel.SetAddress(config.Gateway.Address) 92 | 93 | tunnel.ClearAllLinks() 94 | for _, light := range config.Lights { 95 | setupLight(node, tunnel, light) 96 | } 97 | 98 | for _, sensor := range config.Sensors { 99 | setupSensor(node, tunnel, sensor) 100 | } 101 | 102 | return nil 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pkg/installer/user.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "strconv" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func CreateUser(username string) { 12 | action := "Check user '" + username + "'" 13 | if userExists(username) { 14 | logrus.WithFields(logrus.Fields{ 15 | "exists": "true", 16 | }).Debug(action) 17 | return 18 | } 19 | 20 | out, err := Run("useradd", "-m", "-r", "-s", "/bin/false", username) 21 | if err != nil { 22 | logrus.WithFields(logrus.Fields{ 23 | "exists": "false", 24 | "error": err, 25 | "output": out, 26 | }).Panic(action) 27 | return 28 | } 29 | 30 | logrus.WithFields(logrus.Fields{ 31 | "exists": "false", 32 | "created": "true", 33 | }).Info(action) 34 | } 35 | 36 | func userExists(username string) bool { 37 | _, err := Run("id", "-u", username) 38 | if err != nil { 39 | return false 40 | } 41 | return true 42 | } 43 | 44 | func CreateDirAsUser(directory string, username string) { 45 | action := "Check directory " + directory 46 | created := false 47 | 48 | if _, err := os.Stat(directory); os.IsNotExist(err) { 49 | err := os.MkdirAll(directory, 0777) 50 | if err != nil { 51 | logrus.WithFields(logrus.Fields{ 52 | "exists": "false", 53 | "error": err, 54 | }).Panic(action) 55 | return 56 | } 57 | created = true 58 | } 59 | 60 | u, err := user.Lookup(username) 61 | if err != nil { 62 | logrus.WithFields(logrus.Fields{ 63 | "exists": "true", 64 | "step": "User lookup", 65 | "error": err, 66 | }).Panic(action) 67 | return 68 | } 69 | 70 | uid, err := strconv.Atoi(u.Uid) 71 | if err != nil { 72 | logrus.WithFields(logrus.Fields{ 73 | "exists": "true", 74 | "step": "strconv.Atoi(u.Uid)", 75 | "error": err, 76 | }).Panic(action) 77 | return 78 | } 79 | gid, err := strconv.Atoi(u.Gid) 80 | if err != nil { 81 | logrus.WithFields(logrus.Fields{ 82 | "exists": "true", 83 | "step": "strconv.Atoi(u.Gid)", 84 | "error": err, 85 | }).Panic(action) 86 | return 87 | } 88 | err = os.Chown(directory, uid, gid) 89 | if err != nil { 90 | logrus.WithFields(logrus.Fields{ 91 | "exists": "true", 92 | "step": "Set permissions", 93 | "error": err, 94 | }).Panic(action) 95 | return 96 | } 97 | 98 | if created { 99 | logrus.WithFields(logrus.Fields{ 100 | "created": "true", 101 | }).Info(action) 102 | } else { 103 | logrus.WithFields(logrus.Fields{ 104 | "exists": "true", 105 | }).Debug(action) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /nodes/stampzilla-server/web/src/routes/automation/components/StateEditor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AlphaPicker } from 'react-color'; 3 | import SliderPointer from 'react-color/lib/components/slider/SliderPointer'; 4 | import Switch from 'react-switch'; 5 | import ct from 'color-temperature'; 6 | 7 | import { uniqeId } from '../../../helpers'; 8 | 9 | // import HueColorPicker from './HueColorPicker'; 10 | 11 | const temperatureToRgb = (temp) => { 12 | const color = ct.colorTemperature2rgb(temp); 13 | return `rgb(${color.red}, ${color.green}, ${color.blue})`; 14 | }; 15 | 16 | const temperatureGradient = (start = 2000, end = 6000, steps = 10) => { 17 | const grad = []; 18 | for (let i = 0; i <= steps; i += 1) { 19 | const temp = ((end - start) / steps) * i; 20 | grad.push(`${temperatureToRgb(temp + start)} ${(100 / steps) * i}%`); 21 | } 22 | 23 | return grad.join(', '); 24 | }; 25 | 26 | class StateEditor extends Component { 27 | renderStateEditor() { 28 | const { 29 | state, onChange, device, arrayKey, 30 | } = this.props; 31 | const id = uniqeId(); 32 | const type = typeof state; 33 | 34 | switch (type) { 35 | case 'boolean': 36 | return ( 37 | 44 | ); 45 | 46 | case 'Brightness': 47 | case 'Volume': 48 | return ( 49 | onChange(rgb.a)} 65 | disabled={!device.get('online') || !onChange} 66 | /> 67 | ); 68 | default: 69 | return ( 70 | onChange(device, arrayKey, event.target.value)} 75 | /> 76 | ); 77 | } 78 | } 79 | 80 | render() { 81 | return <>{this.renderStateEditor()}; 82 | } 83 | } 84 | 85 | export default StateEditor; 86 | --------------------------------------------------------------------------------