├── LICENSE ├── README.md ├── mhplayer ├── http.go ├── record.go └── static │ ├── demo.css │ ├── index.html │ ├── index.html.txt │ ├── indexnew.html │ └── indexnew0.html ├── mhrecorder ├── main.go └── static │ ├── demo.css │ └── index.html ├── mhserver ├── client.go ├── config.go ├── crontab ├── globals.go ├── home.html ├── hub.go ├── main.go ├── processmsg.go ├── scheduler.go └── view │ ├── cameras.html │ ├── config │ ├── Hello.json │ ├── New.json │ ├── connected.json │ ├── dirtest.json │ ├── eastshoplightoff.json │ ├── eastshoplighton.json │ ├── lunch.json │ ├── motiondetectoff.json │ ├── motiondetecton.json │ ├── mute.json │ ├── nightmodeoff.json │ ├── nightmodeon.json │ ├── northshoplightoff.json │ ├── northshoplighton.json │ ├── note.json │ ├── restartclient.json │ ├── setshopthermostat.json │ ├── southshoplightoff.json │ ├── southshoplighton.json │ ├── unmute.json │ ├── volume-100.json │ ├── volume-25.json │ ├── volume-50.json │ ├── volume-75.json │ └── volume.json │ ├── control.html │ ├── css │ ├── bulma-switch.min.css │ ├── img │ │ └── jsoneditor-icons.png │ ├── jsoneditor.css │ └── style.css │ ├── index.html │ ├── peer.html │ └── scripts │ ├── control.js │ ├── img │ └── jsoneditor-icons.png │ ├── jsoneditor.js │ ├── jstree.js │ ├── peer.js │ ├── seq.js │ └── single.js ├── mycam ├── README.md ├── fifo.go ├── http.go.txt ├── main.go ├── recorder.go ├── rtc.go ├── rtsp2webrtc.go ├── static │ ├── demo.css │ └── index.html └── webrtc.png └── pivoice ├── main.go └── picgotts.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 machinesworking 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # myhome 2 | A Home grown security system using Google Go, Raspberry Pi, and hacked Wizecams using 3 | Dafang hacks at https://github.com/Dafang-Hacks/rootfs 4 | 5 | The system is intentionally local only .....no cloud 6 | 7 | Audible Notifications use picotts on wireless pi zeros 8 | 9 | Lights are controlled using hacked sonoff switches 10 | 11 | A thermostat is built from an esp8266 12 | 13 | My Home server is written in go and uses websockets for device communications https://github.com/gorilla/websocket 14 | 15 | Control is via web page using json messages. 16 | 17 | Configuration is also a web page. 18 | 19 | All devices are discovered automatically by the server connection and the control page is built dynamically. 20 | 21 | The Wizecams use onboard RtsptoWebrtc from https://github.com/deepch/RTSPtoWebRTC 22 | 23 | Which uses https://github.com/pions/webrtc for the webrtc peer written entirely in Go. 24 | 25 | The cameras can be viewed in a web page using the supplied html and javascript using webrtc. 26 | 27 | Disclaimer: This is my first attempt at this kind of project. Use at your own risk. I will be adding more documentation to the code as I clean it up. 28 | -------------------------------------------------------------------------------- /mhplayer/http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "encoding/base64" 10 | "encoding/json" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/pions/webrtc" 14 | "github.com/pions/webrtc/pkg/ice" 15 | ) 16 | 17 | //var DataChanelTest chan<- media.RTCSample 18 | 19 | //var rawrtpchan chan<- *rtp.Packet 20 | var namechan = make(chan string) 21 | var playctrlchan = make(chan string) 22 | var vp8Track *webrtc.RTCTrack 23 | 24 | func main() { 25 | dir, _ := os.Getwd() 26 | 27 | fmt.Printf("CWD: %s\n", dir) 28 | r := mux.NewRouter() 29 | r.HandleFunc("/receive", home) 30 | r.HandleFunc("/list", list) 31 | r.HandleFunc("/play", play) 32 | r.HandleFunc("/stop", stop) 33 | 34 | r.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.Dir("static/")))) 35 | go playgob(playctrlchan) 36 | go func() { 37 | err := http.ListenAndServe(":8181", r) 38 | if err != nil { 39 | } 40 | }() 41 | select {} 42 | } 43 | 44 | func list(w http.ResponseWriter, r *http.Request) { 45 | data := r.FormValue("data") 46 | list, err := IOReadDir(data) 47 | if err != nil { 48 | fmt.Println("list failed: " + err.Error()) 49 | 50 | } 51 | filelist, err := json.Marshal(list) 52 | if err != nil { 53 | fmt.Println("Error: " + err.Error()) 54 | } 55 | w.Write(filelist) 56 | } 57 | 58 | func play(w http.ResponseWriter, r *http.Request) { 59 | 60 | data := r.FormValue("data") 61 | namechan <- data 62 | } 63 | func stop(w http.ResponseWriter, r *http.Request) { 64 | 65 | playctrlchan <- "stop" 66 | } 67 | func home(w http.ResponseWriter, r *http.Request) { 68 | w.Header().Set("Access-Control-Allow-Origin", "*") 69 | data := r.FormValue("data") 70 | sd, err := base64.StdEncoding.DecodeString(data) 71 | 72 | if err != nil { 73 | log.Println(err) 74 | return 75 | } 76 | webrtc.RegisterDefaultCodecs() 77 | peerConnection, err := webrtc.New(webrtc.RTCConfiguration{ 78 | IceServers: []webrtc.RTCIceServer{ 79 | { 80 | URLs: []string{"stun:stun.l.google.com:19302"}, 81 | }, 82 | }, 83 | }) 84 | if err != nil { 85 | panic(err) 86 | } 87 | peerConnection.OnICEConnectionStateChange(func(connectionState ice.ConnectionState) { 88 | fmt.Printf("Connection State has changed %s \n", connectionState.String()) 89 | }) 90 | vp8Track, err = peerConnection.NewRTCSampleTrack(webrtc.DefaultPayloadTypeH264, "video", "pion2") 91 | 92 | if err != nil { 93 | log.Println(err) 94 | return 95 | } 96 | _, err = peerConnection.AddTrack(vp8Track) 97 | if err != nil { 98 | log.Println(err) 99 | return 100 | } 101 | offer := webrtc.RTCSessionDescription{ 102 | Type: webrtc.RTCSdpTypeOffer, 103 | Sdp: string(sd), 104 | } 105 | if err := peerConnection.SetRemoteDescription(offer); err != nil { 106 | log.Println(err) 107 | return 108 | } 109 | answer, err := peerConnection.CreateAnswer(nil) 110 | if err != nil { 111 | log.Println(err) 112 | return 113 | } 114 | w.Write([]byte(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))) 115 | // DataChanelTest = vp8Track.Samples 116 | 117 | } 118 | -------------------------------------------------------------------------------- /mhplayer/record.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/gob" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "time" 9 | 10 | "github.com/pions/webrtc/pkg/media" 11 | ) 12 | 13 | // This saves the gob from the time the the motion signal is sent until it times out 14 | func savegob(rtspchan chan media.RTCSample, savectrlchan chan string, devicename string) { 15 | var path = "static/clips/" + devicename 16 | var file *os.File 17 | var enc *gob.Encoder 18 | var state = "stopped" 19 | defer func() { 20 | file.Close() 21 | }() 22 | 23 | for { 24 | select { 25 | case gob := <-rtspchan: 26 | if state == "started" { 27 | enc.Encode(gob) 28 | } 29 | case ctrl := <-savectrlchan: 30 | switch ctrl { 31 | 32 | case "stop": 33 | state = "stopped" 34 | 35 | case "start": 36 | file, err := os.Create(path + "/" + devicename + time.Now().Format(time.RFC3339) + ".gob") 37 | if err != nil { 38 | fmt.Printf("Could not create file: %s", err.Error()) 39 | return 40 | } 41 | // fmt.Println("Received start and created file for ") 42 | enc = gob.NewEncoder(file) 43 | state = "started" 44 | case "destroy": 45 | return 46 | 47 | } 48 | } 49 | } 50 | 51 | } 52 | 53 | // playgob plays the recorded gob to the browser 54 | func playgob(playctrlchan chan string) { 55 | var state = "stopped" 56 | var file *os.File 57 | var path = "static/clips/" 58 | var decoder *gob.Decoder 59 | defer func() { 60 | file.Close() 61 | }() 62 | var err error 63 | ticker := time.NewTicker(time.Millisecond * 25) 64 | 65 | // var cnt = 0 66 | for { 67 | select { 68 | case filename := <-namechan: 69 | if state == "playing" { 70 | fmt.Println("Canceled Play") 71 | file.Close() 72 | state = "stopped" 73 | } 74 | file, err = os.Open(path + filename) 75 | if err != nil { 76 | fmt.Printf("Could not open file: %s", err.Error()) 77 | continue 78 | } 79 | decoder = gob.NewDecoder(file) 80 | 81 | fmt.Println("Changed state to playing for file: " + filename) 82 | state = "playing" 83 | case ctrl := <-playctrlchan: 84 | if ctrl == "stop" { 85 | if state == "playing" { 86 | fmt.Println("Canceled Play") 87 | file.Close() 88 | state = "stopped" 89 | 90 | } 91 | } 92 | case <-ticker.C: 93 | if state == "playing" { 94 | goblet := new(media.RTCSample) 95 | 96 | err = decoder.Decode(goblet) 97 | if err != nil { 98 | if err.Error() == "EOF" { 99 | file.Seek(0, 0) 100 | fmt.Println("Looping") 101 | decoder = gob.NewDecoder(file) 102 | 103 | } else { 104 | fmt.Printf("Decode GOB Error: %s", err.Error()) 105 | continue 106 | } 107 | 108 | } 109 | 110 | vp8Track.Samples <- *goblet 111 | } 112 | } 113 | } 114 | 115 | } 116 | 117 | func IOReadDir(root string) ([]string, error) { 118 | var files []string 119 | var path = "static/clips/" 120 | fileInfo, err := ioutil.ReadDir(path + root) 121 | if err != nil { 122 | return files, err 123 | } 124 | for _, file := range fileInfo { 125 | files = append(files, file.Name()) 126 | } 127 | return files, nil 128 | } 129 | -------------------------------------------------------------------------------- /mhplayer/static/demo.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mhplayer/static/index.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Browser base64 Session Description
4 | Golang base64 Session Description:
5 | 6 | 7 |
8 | 11 |
12 |
13 | 83 | -------------------------------------------------------------------------------- /mhplayer/static/index.html.txt: -------------------------------------------------------------------------------- 1 | 2 |

3 | Browser base64 Session Description
4 | Golang base64 Session Description:
5 | 6 | 7 |
8 | 57 | -------------------------------------------------------------------------------- /mhplayer/static/indexnew.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Browser base64 Session Description
4 | Golang base64 Session Description:
5 | 6 | 7 |
8 | 15 | 16 | 17 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 110 | -------------------------------------------------------------------------------- /mhplayer/static/indexnew0.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Browser base64 Session Description
4 | Golang base64 Session Description:
5 | 6 | 7 |
8 | 15 | 16 | 17 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 118 | -------------------------------------------------------------------------------- /mhrecorder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/gob" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/pions/webrtc/pkg/media" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | const ( 19 | // Port is the port number that the server listens to. 20 | Port = ":61000" 21 | ) 22 | 23 | /* 24 | ## Incoming connections 25 | 26 | Preparing for incoming data is a bit more involved. According to our ad-hoc 27 | protocol, we receive the name of a command terminated by `\n`, followed by data. 28 | The nature of the data depends on the respective command. To handle this, we 29 | create an `Endpoint` object with the following properties: 30 | 31 | * It allows to register one or more handler functions, where each can handle a 32 | particular command. 33 | * It dispatches incoming commands to the associated handler based on the commands 34 | name. 35 | 36 | */ 37 | 38 | // HandleFunc is a function that handles an incoming command. 39 | // It receives the open connection wrapped in a `ReadWriter` interface. 40 | type HandleFunc func(*bufio.ReadWriter) 41 | 42 | var file *os.File 43 | var sample media.RTCSample 44 | var path = "static/clips/" 45 | 46 | // Endpoint provides an endpoint to other processess 47 | // that they can send data to. 48 | type Endpoint struct { 49 | listener net.Listener 50 | handler map[string]HandleFunc 51 | 52 | // Maps are not threadsafe, so we need a mutex to control access. 53 | m sync.RWMutex 54 | } 55 | 56 | // NewEndpoint creates a new endpoint. To keep things simple, 57 | // the endpoint listens on a fixed port number. 58 | func NewEndpoint() *Endpoint { 59 | // Create a new Endpoint with an empty list of handler funcs. 60 | return &Endpoint{ 61 | handler: map[string]HandleFunc{}, 62 | } 63 | } 64 | 65 | // AddHandleFunc adds a new function for handling incoming data. 66 | func (e *Endpoint) AddHandleFunc(name string, f HandleFunc) { 67 | e.m.Lock() 68 | e.handler[name] = f 69 | e.m.Unlock() 70 | } 71 | 72 | // Listen starts listening on the endpoint port on all interfaces. 73 | // At least one handler function must have been added 74 | // through AddHandleFunc() before. 75 | func (e *Endpoint) Listen() error { 76 | var err error 77 | e.listener, err = net.Listen("tcp", Port) 78 | if err != nil { 79 | return errors.Wrapf(err, "Unable to listen on port %s\n", Port) 80 | } 81 | log.Println("Listen on", e.listener.Addr().String()) 82 | for { 83 | log.Println("Accept a connection request.") 84 | conn, err := e.listener.Accept() 85 | if err != nil { 86 | log.Println("Failed accepting a connection request:", err) 87 | continue 88 | } 89 | log.Println("Handle incoming messages.") 90 | go e.handleMessages(conn) 91 | } 92 | } 93 | 94 | // handleMessages reads the connection up to the first newline. 95 | // Based on this string, it calls the appropriate HandleFunc. 96 | func (e *Endpoint) handleMessages(conn net.Conn) { 97 | // Wrap the connection into a buffered reader for easier reading. 98 | rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) 99 | defer conn.Close() 100 | 101 | // Read from the connection until EOF. Expect a command name as the 102 | // next input. Call the handler that is registered for this command. 103 | for { 104 | log.Print("Receive command '") 105 | cmd, err := rw.ReadString('\n') 106 | switch { 107 | case err == io.EOF: 108 | log.Println("Reached EOF - close this connection.\n ---") 109 | return 110 | case err != nil: 111 | log.Println("\nError reading command. Got: '"+cmd+"'\n", err) 112 | return 113 | } 114 | // Trim the request string - ReadString does not strip any newlines. 115 | cmd = strings.Trim(cmd, "\n ") 116 | log.Println(cmd + "'") 117 | 118 | // Fetch the appropriate handler function from the 'handler' map and call it. 119 | e.m.RLock() 120 | handleCommand, ok := e.handler[cmd] 121 | e.m.RUnlock() 122 | if !ok { 123 | log.Println("Command '" + cmd + "' is not registered.") 124 | return 125 | } 126 | handleCommand(rw) 127 | } 128 | } 129 | 130 | /* Now let's create two handler functions. The easiest case is where our 131 | ad-hoc protocol only sends string data. 132 | 133 | The second handler receives and processes a struct that was sent as GOB data. 134 | */ 135 | 136 | // handleStrings handles the "STRING" request. 137 | 138 | func handleOpen(rw *bufio.ReadWriter) { 139 | // Receive a string. 140 | 141 | log.Print("Receive STRING message:") 142 | s, err := rw.ReadString('\n') 143 | if err != nil { 144 | log.Println("Cannot read from connection.\n", err) 145 | } 146 | s = strings.Trim(s, "\n ") 147 | log.Println(s) 148 | file, err = os.Create(path + s) 149 | if err != nil { 150 | log.Printf("Could not create file: %s", err.Error()) 151 | _, err = rw.WriteString("Error\n") 152 | if err != nil { 153 | log.Println("Cannot write to connection.\n", err) 154 | } 155 | err = rw.Flush() 156 | if err != nil { 157 | log.Println("Flush failed.", err) 158 | } 159 | return 160 | } 161 | 162 | _, err = rw.WriteString("continue\n") 163 | if err != nil { 164 | log.Println("Cannot write to connection.\n", err) 165 | } 166 | err = rw.Flush() 167 | if err != nil { 168 | log.Println("Flush failed.", err) 169 | } 170 | 171 | } 172 | 173 | func handleRecord(rw *bufio.ReadWriter) { 174 | log.Print("Recording GOB data:") 175 | // sample := make([]byte, 1024) 176 | dec := gob.NewDecoder(rw) 177 | enc := gob.NewEncoder(file) 178 | goblet := new(media.RTCSample) 179 | for { 180 | 181 | err := dec.Decode(goblet) 182 | if err != nil { 183 | file.Sync() 184 | file.Close() 185 | fmt.Println("Closing reader") 186 | return 187 | } 188 | 189 | //_, err = file.Write(sample) 190 | err = enc.Encode(goblet) 191 | if err != nil { 192 | file.Sync() 193 | file.Close() 194 | fmt.Println("Closing writer") 195 | return 196 | } 197 | } 198 | } 199 | 200 | /* 201 | ## The client and server functions 202 | 203 | With all this in place, we can now set up client and server functions. 204 | 205 | The client function connects to the server and sends STRING and GOB requests. 206 | 207 | The server starts listening for requests and triggers the appropriate handlers. 208 | */ 209 | 210 | // server listens for incoming requests and dispatches them to 211 | // registered handler functions. 212 | func server() error { 213 | endpoint := NewEndpoint() 214 | 215 | // Add the handle funcs. 216 | endpoint.AddHandleFunc("OPEN", handleOpen) 217 | endpoint.AddHandleFunc("RECORD", handleRecord) 218 | 219 | // Start listening. 220 | return endpoint.Listen() 221 | } 222 | 223 | /* 224 | ## Main 225 | 226 | Main starts either a client or a server, depending on whether the `connect` 227 | flag is set. Without the flag, the process starts as a server, listening 228 | for incoming requests. With the flag the process starts as a client and connects 229 | to the host specified by the flag value. 230 | 231 | Try "localhost" or "127.0.0.1" when running both processes on the same machine. 232 | 233 | */ 234 | 235 | // main 236 | func main() { 237 | 238 | err := server() 239 | if err != nil { 240 | log.Println("Error:", errors.WithStack(err)) 241 | } 242 | 243 | log.Println("Server done.") 244 | } 245 | 246 | // The Lshortfile flag includes file name and line number in log messages. 247 | func init() { 248 | log.SetFlags(log.Lshortfile) 249 | } 250 | -------------------------------------------------------------------------------- /mhrecorder/static/demo.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mhrecorder/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Myhome Recorder 8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | Browser base64 Session Description
16 | Golang base64 Session Description:
17 | 18 | 19 |
20 | 27 | 28 | 29 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /mhserver/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | const ( 14 | // Time allowed to write a message to the peer. 15 | writeWait = 2 * time.Second 16 | 17 | // Time allowed to read the next pong message from the peer. 18 | pongWait = 60 * time.Second 19 | 20 | // Send pings to peer with this period. Must be less than pongWait. 21 | pingPeriod = (pongWait * 9) / 10 22 | 23 | // Maximum message size allowed from peer. 24 | maxMessageSize = 10000 25 | ) 26 | 27 | var ( 28 | newline = []byte{'\n'} 29 | space = []byte{' '} 30 | ) 31 | 32 | var upgrader = websocket.Upgrader{ 33 | ReadBufferSize: 4096, 34 | WriteBufferSize: 4096, 35 | } 36 | 37 | // readPump pumps messages from the websocket connection to the hub. 38 | // 39 | // The application runs readPump in a per-connection goroutine. The application 40 | // ensures that there is at most one reader on a connection by executing all 41 | // reads from this goroutine. 42 | func (c *Client) readPump() { 43 | defer func() { 44 | log.Println("closing websocket read") 45 | ti := time.Now() 46 | note := rawmsg{Type: "notification", Value: ti.Format(time.Stamp) + ": " + c.device.Desc + " has disconnected"} 47 | 48 | jnote, err := json.Marshal(note) 49 | if err != nil { 50 | log.Printf("Marshal error: %s", err.Error()) 51 | } 52 | mesg := message{Type: "control", Targets: []string{"text"}, Msg: jnote} 53 | c.hub.broadcast <- mesg 54 | 55 | c.hub.unregister <- c 56 | c.conn.Close() 57 | }() 58 | c.conn.SetReadLimit(maxMessageSize) 59 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 60 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 61 | 62 | for { 63 | mtype, msg, err := c.conn.ReadMessage() 64 | if err != nil { 65 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 66 | log.Printf("error: %v", err) 67 | } 68 | break 69 | } 70 | 71 | if mtype == websocket.TextMessage { 72 | var messg message 73 | err = json.Unmarshal(msg, &messg) 74 | if err != nil { 75 | log.Printf("unmarshal error: %s", err.Error()) 76 | } 77 | c.msg = &messg 78 | processchan <- c 79 | 80 | } 81 | } 82 | } 83 | 84 | // writePump pumps messages from the hub to the websocket connection. 85 | // 86 | // A goroutine running writePump is started for each connection. The 87 | // application ensures that there is at most one writer to a connection by 88 | // executing all writes from this goroutine. 89 | func (c *Client) writePump() { 90 | ticker := time.NewTicker(pingPeriod) 91 | defer func() { 92 | log.Println("closing websocket write") 93 | ticker.Stop() 94 | c.conn.Close() 95 | }() 96 | for { 97 | select { 98 | case msg, ok := <-c.send: 99 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 100 | if !ok { 101 | log.Println("hub closed channel") // The hub closed the channel. 102 | ti := time.Now() 103 | note := rawmsg{Type: "notification", Value: ti.Format(time.Stamp) + ": " + c.device.Desc + " was disconnected by the Hub "} 104 | 105 | jnote, err := json.Marshal(note) 106 | if err != nil { 107 | log.Printf("Marshal error: %s", err.Error()) 108 | } 109 | mesg := message{Type: "control", Targets: []string{"text"}, Msg: jnote} 110 | c.hub.broadcast <- mesg 111 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 112 | return 113 | } 114 | w, err := c.conn.NextWriter(websocket.TextMessage) 115 | if err != nil { 116 | log.Printf("couldn't get next writer: %s \n", err.Error()) 117 | return 118 | } 119 | 120 | msgstr, _ := json.Marshal(msg) 121 | 122 | // websocket.WriteJSON(c.conn, msg) 123 | w.Write(msgstr) 124 | 125 | // Add queued chat messages to the current websocket message. 126 | n := len(c.send) 127 | for i := 0; i < n; i++ { 128 | 129 | websocket.WriteJSON(c.conn, <-c.send) 130 | } 131 | 132 | if err := w.Close(); err != nil { 133 | return 134 | } 135 | 136 | case <-ticker.C: 137 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 138 | 139 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 140 | log.Println("couldn't send ping") 141 | return 142 | } 143 | // default: 144 | // fmt.Println("dropped message") 145 | } 146 | } 147 | } 148 | 149 | // serveWs handles websocket requests from the peer. 150 | func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { 151 | log.Println(r) 152 | conn, err := upgrader.Upgrade(w, r, nil) 153 | if err != nil { 154 | 155 | log.Println(err) 156 | return 157 | } 158 | host, _, _ := net.SplitHostPort(r.RemoteAddr) 159 | 160 | device := &device{Type: "", Name: "", Desc: "", State: "", Address: host} 161 | client := &Client{hub: hub, conn: conn, send: make(chan message), device: device} 162 | client.hub.register <- client 163 | 164 | // Allow collection of memory referenced by the caller by doing all work in 165 | // new goroutines. 166 | go client.writePump() 167 | go client.readPump() 168 | 169 | } 170 | -------------------------------------------------------------------------------- /mhserver/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "strings" 9 | 10 | "os" 11 | ) 12 | 13 | //load the configuration 14 | func loadnodes(nodesFile string) ([]byte, error) { 15 | 16 | //read the entire file into var body 17 | body, err := ioutil.ReadFile(msgfilepath + "/" + nodesFile) 18 | 19 | if err != nil { 20 | fmt.Printf("Couldn't load nodes file\r\n") 21 | return nil, err 22 | } 23 | 24 | return body, nil 25 | } 26 | 27 | func loadmsgnames() (*message, error) { 28 | files := make(map[string]json.RawMessage) 29 | // names := []string{} 30 | //read the configs in the config folder 31 | fileinfos, err := ioutil.ReadDir(msgfilepath) 32 | if err != nil { 33 | fmt.Printf("Couldn't load config file names file\r\n") 34 | return nil, err 35 | } 36 | 37 | // list only the JSON files...no folders 38 | for _, info := range fileinfos { 39 | if info.IsDir() { 40 | continue 41 | } 42 | if strings.HasSuffix(info.Name(), ".json") { 43 | //names = append(names, info.Name()) 44 | txt, _ := loadnodes(info.Name()) 45 | 46 | files[info.Name()] = txt 47 | } 48 | } 49 | //create the map for the JSON message 50 | 51 | msg := new(message) 52 | msg.Type = "control" 53 | ctrl := rawmsg{Type: "msglist", Value: files} 54 | 55 | ctrlstr, err := json.Marshal(ctrl) 56 | if err != nil { 57 | fmt.Printf("could not marshal controls: %s", err.Error()) 58 | return nil, err 59 | } 60 | 61 | msg.Targets = []string{"text", "hmi"} 62 | msg.Msg = ctrlstr 63 | 64 | return msg, nil 65 | } 66 | 67 | //Save a changed Config file 68 | func saveNodes(nodesFile string, msg interface{}) { 69 | 70 | data, err := json.Marshal(msg) 71 | if err != nil { 72 | log.Printf("Could not save file: %s", nodesFile) 73 | } 74 | 75 | //write the File to the correct path 76 | fmt.Printf("Saving file %s with %s\n", msgfilepath+"/"+nodesFile, data) 77 | err = ioutil.WriteFile(msgfilepath+"/"+nodesFile, data, os.FileMode.Perm(0666)) 78 | if err != nil { 79 | fmt.Printf("failed to save message: %s", err.Error()) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /mhserver/crontab: -------------------------------------------------------------------------------- 1 | 30 * * * * * * printme 2 | -------------------------------------------------------------------------------- /mhserver/globals.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gorilla/websocket" 7 | "github.com/pions/webrtc" 8 | ) 9 | 10 | var msgfilepath = "view/config" 11 | var processchan = make(chan *Client) 12 | var answerchan = make(chan webrtc.RTCSessionDescription) 13 | 14 | // Client is a middleman between the websocket connection and the hub. 15 | type Client struct { 16 | hub *Hub 17 | 18 | // The websocket connection. 19 | conn *websocket.Conn 20 | 21 | // Buffered channel of outbound messages. 22 | send chan message 23 | msg *message 24 | device *device 25 | } 26 | 27 | type device struct { 28 | Type string 29 | Name string 30 | Desc string 31 | State string 32 | Address string 33 | } 34 | 35 | type command struct { 36 | Name string 37 | Value map[string]interface{} 38 | } 39 | type message struct { 40 | Type string 41 | Targets []string 42 | //Conn *websocket.Conn 43 | Msg json.RawMessage 44 | } 45 | type rawmsg struct { 46 | Type string 47 | Value interface{} 48 | } 49 | -------------------------------------------------------------------------------- /mhserver/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat Example 5 | 53 | 90 | 91 | 92 |
93 |
94 | 95 | 96 |
97 | 98 | 99 | -------------------------------------------------------------------------------- /mhserver/hub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // Hub maintains the set of active clients and broadcasts messages to the 8 | // clients. 9 | type Hub struct { 10 | // Registered clients. 11 | clients map[*Client]bool 12 | 13 | // Inbound messages from the clients. 14 | broadcast chan message 15 | process chan *Client 16 | 17 | // Register requests from the clients. 18 | register chan *Client 19 | 20 | // Unregister requests from clients. 21 | unregister chan *Client 22 | } 23 | 24 | func newHub() *Hub { 25 | return &Hub{ 26 | broadcast: make(chan message), 27 | process: make(chan *Client), 28 | register: make(chan *Client), 29 | unregister: make(chan *Client), 30 | clients: make(map[*Client]bool), 31 | } 32 | } 33 | 34 | func (h *Hub) run() { 35 | for { 36 | select { 37 | case client := <-h.register: 38 | h.clients[client] = true 39 | case client := <-h.unregister: 40 | if _, ok := h.clients[client]; ok { 41 | delete(h.clients, client) 42 | close(client.send) 43 | } 44 | case msg := <-h.broadcast: 45 | // log.Println("broadcasting") 46 | for client := range h.clients { 47 | // log.Printf("found %s\n", client.device.Name) 48 | for _, targ := range msg.Targets { 49 | if client.device.Type == targ || client.device.Name == targ { 50 | select { 51 | case client.send <- msg: 52 | default: 53 | log.Printf("Hub closing client %s\n", client.device.Name) 54 | close(client.send) 55 | delete(h.clients, client) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /mhserver/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | ) 13 | 14 | var addr = flag.String("addr", ":8080", "http service address") 15 | 16 | func serveHome(w http.ResponseWriter, r *http.Request) { 17 | log.Println(r.URL) 18 | if r.URL.Path != "/" { 19 | http.Error(w, "Not found", http.StatusNotFound) 20 | return 21 | } 22 | if r.Method != "GET" { 23 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 24 | return 25 | } 26 | http.ServeFile(w, r, "home.html") 27 | } 28 | 29 | func main() { 30 | flag.Parse() 31 | hub := newHub() 32 | go hub.run() 33 | go processmsg() 34 | go http.Handle("/", http.FileServer(http.Dir("view/"))) 35 | go scheduler() 36 | 37 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 38 | serveWs(hub, w, r) 39 | }) 40 | fmt.Println("starting server") 41 | err := http.ListenAndServe(*addr, nil) 42 | if err != nil { 43 | log.Fatal("ListenAndServe: ", err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mhserver/processmsg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | uuid "github.com/satori/go.uuid" 11 | ) 12 | 13 | func processmsg() { 14 | for { 15 | select { 16 | case c := <-processchan: 17 | if c.msg.Type == "status" { 18 | // update device info with status msg 19 | var dev device 20 | 21 | err := json.Unmarshal(c.msg.Msg, &dev) 22 | if err != nil { 23 | log.Printf("unmarshal error: %s", err.Error()) 24 | } 25 | dev.Address = c.device.Address 26 | c.device = &dev 27 | 28 | ti := time.Now() 29 | //send notification to browsers of status 30 | note := rawmsg{Type: "notification", Value: ti.Format(time.Stamp) + ": " + c.device.Desc + " connected"} 31 | jnote, err := json.Marshal(note) 32 | if err != nil { 33 | log.Printf("Marshal error: %s", err.Error()) 34 | } 35 | 36 | // load the saved message names to the browsers 37 | mesg := message{Type: "control", Targets: []string{"text"}, Msg: jnote} 38 | c.hub.broadcast <- mesg 39 | if c.device.Type == "text" { 40 | msglist, _ := loadmsgnames() 41 | c.send <- *msglist 42 | 43 | ms := new(message) 44 | ms.Type = "control" 45 | var devs []string 46 | for client := range c.hub.clients { 47 | if client.device.Type == "camera" { 48 | devs = append(devs, client.device.Name) 49 | } 50 | } 51 | note := rawmsg{Type: "peerlist", Value: devs} 52 | 53 | ms.Msg, _ = json.Marshal(note) 54 | c.send <- *ms 55 | id, _ := uuid.NewV4() 56 | c.device.Name = id.String() 57 | note = rawmsg{Type: "id", Value: id} 58 | ms.Msg, _ = json.Marshal(note) 59 | c.send <- *ms 60 | 61 | } 62 | 63 | // send new camera control to the recorder 64 | if c.device.Type == "camera" { 65 | recnote := rawmsg{Type: "create", Value: c.device} 66 | jrecnote, err := json.Marshal(recnote) 67 | if err != nil { 68 | log.Printf("Marshal error: %s", err.Error()) 69 | } 70 | recmesg := message{Type: "control", Targets: []string{"recorder"}, Msg: jrecnote} 71 | c.hub.broadcast <- recmesg 72 | } 73 | // if the device is the recorder send currently connected cameras 74 | if c.device.Type == "recorder" { 75 | for client := range c.hub.clients { 76 | log.Printf("found %s\n", client.device.Name) 77 | 78 | if client.device.Type == "camera" { 79 | recnote := rawmsg{Type: "create", Value: client.device} 80 | jrecnote, err := json.Marshal(recnote) 81 | if err != nil { 82 | log.Printf("Marshal error: %s", err.Error()) 83 | } 84 | recmesg := message{Type: "control", Targets: []string{"recorder"}, Msg: jrecnote} 85 | c.send <- recmesg 86 | 87 | } 88 | 89 | } 90 | } 91 | 92 | } 93 | 94 | if c.msg.Type == "motion" { 95 | 96 | val := new(rawmsg) 97 | err := json.Unmarshal(c.msg.Msg, val) 98 | if err != nil { 99 | fmt.Printf("failed to decode motion\n") 100 | 101 | } 102 | // send motion start to the device node in the recorder 103 | if val.Value.(string) == "on" { 104 | rctrl := rawmsg{Type: "start", Value: c.device} 105 | jrctrl, _ := json.Marshal(rctrl) 106 | 107 | rmesg := message{Type: "control", Targets: []string{"recorder"}, Msg: jrctrl} 108 | c.hub.broadcast <- rmesg 109 | // send motion start to angelos and browsers 110 | note := rawmsg{Type: "notification", Value: "motion detected " + c.device.Desc} 111 | jnote, _ := json.Marshal(note) 112 | mesg := message{Type: "control", Targets: []string{"hmi", "text"}, Msg: jnote} 113 | c.hub.broadcast <- mesg 114 | 115 | } 116 | //same as above for off command 117 | if val.Value.(string) == "off" { 118 | note := rawmsg{Type: "stop", Value: c.device} 119 | jnote, _ := json.Marshal(note) 120 | 121 | mesg := message{Type: "control", Targets: []string{"recorder"}, Msg: jnote} 122 | c.hub.broadcast <- mesg 123 | /* 124 | note1 := rawmsg{Type: "notification", Value: "no motion " + c.device.Desc} 125 | jnote1, _ := json.Marshal(note1) 126 | mesg1 := message{Type: "control", Targets: []string{"hmi", "text"}, Msg: jnote1} 127 | c.hub.broadcast <- mesg1 128 | */ 129 | } 130 | 131 | } 132 | if c.msg.Type == "state" { 133 | 134 | val := new(rawmsg) 135 | err := json.Unmarshal(c.msg.Msg, val) 136 | if err != nil { 137 | fmt.Printf("failed to decode state\n") 138 | 139 | } 140 | 141 | note := rawmsg{Type: "notification", Value: c.device.Desc + "is " + val.Value.(string)} 142 | jnote, _ := json.Marshal(note) 143 | 144 | mesg := message{Type: "control", Targets: []string{"hmi", "text"}, Msg: jnote} 145 | c.hub.broadcast <- mesg 146 | 147 | } 148 | //forward a control message 149 | if c.msg.Type == "control" { 150 | c.hub.broadcast <- *c.msg 151 | 152 | } 153 | 154 | if c.msg.Type == "command" { 155 | //get the command 156 | 157 | var cmd command 158 | err := json.Unmarshal(c.msg.Msg, &cmd) 159 | if err != nil { 160 | fmt.Printf("Could not unmarshal %v\n", cmd) 161 | } 162 | 163 | switch cmd.Name { 164 | 165 | case "savemsg": 166 | // save a new message from browser 167 | log.Printf("Message Value : %v", cmd) 168 | var filename = cmd.Value["Name"].(string) 169 | 170 | var data = cmd.Value["Value"] 171 | 172 | saveNodes(filename, data) 173 | msglist, err := loadmsgnames() 174 | if err != nil { 175 | fmt.Printf("Could not load config list %s", err.Error()) 176 | } 177 | c.send <- *msglist 178 | 179 | // delete a message 180 | case "deletemsg": 181 | filename := cmd.Value["filename"].(string) 182 | // conn := msg["connection"].(*websocket.Conn) 183 | info, err := os.Lstat(msgfilepath + "/" + filename) 184 | if err != nil { 185 | fmt.Println("Can't stat config file") 186 | } else { 187 | if !info.IsDir() { 188 | os.Remove(msgfilepath + "/" + filename) 189 | msglist, err := loadmsgnames() 190 | if err != nil { 191 | fmt.Printf("Could not load config list %s", err.Error()) 192 | } 193 | c.send <- *msglist 194 | } 195 | } 196 | 197 | case "loadmsg": 198 | 199 | // load a message and send to browser 200 | msg := new(message) 201 | var ctrl rawmsg 202 | msg.Type = "control" 203 | ctrl.Type = "configuration" 204 | name := cmd.Value["filename"].(string) 205 | log.Printf("loading filename %s\n", name) 206 | body, _ := loadnodes(name) 207 | 208 | err = json.Unmarshal(body, &ctrl.Value) 209 | if err != nil { 210 | fmt.Printf("could not unmarshal body: %s\n", err.Error()) 211 | } 212 | msg.Msg, err = json.Marshal(ctrl) 213 | if err != nil { 214 | fmt.Printf("could not marshal control value: %s\n", err.Error()) 215 | } 216 | // send the message to the browser 217 | select { 218 | case c.send <- *msg: 219 | default: 220 | close(c.send) 221 | delete(c.hub.clients, c) 222 | } 223 | //send the message from the browser 224 | case "sendmsg": 225 | msg := new(message) 226 | 227 | name := cmd.Value["filename"].(string) 228 | log.Printf("loading filename %s\n", name) 229 | localmsg, _ := loadnodes(name) 230 | 231 | log.Printf("Sending Message: %s", localmsg) 232 | json.Unmarshal(localmsg, msg) 233 | 234 | select { 235 | case c.hub.broadcast <- *msg: 236 | default: 237 | close(c.send) 238 | delete(c.hub.clients, c) 239 | } 240 | case "listdevices": 241 | // list all currently connected devices 242 | 243 | ms := new(message) 244 | ms.Type = "control" 245 | var devs []device 246 | // ti := time.Now() 247 | for client := range c.hub.clients { 248 | if "all" == cmd.Value["Type"].(string) || cmd.Value["Type"].(string) == client.device.Type { 249 | devs = append(devs, *client.device) 250 | } 251 | } 252 | note := rawmsg{Type: "devicelist", Value: devs} 253 | 254 | ms.Msg, _ = json.Marshal(note) 255 | c.send <- *ms 256 | fmt.Printf("Sending device list to %s\n", c.device.Name) 257 | } 258 | 259 | } 260 | 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /mhserver/scheduler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fsnotify/fsnotify" 13 | "github.com/gorhill/cronexpr" 14 | ) 15 | 16 | type schedule struct { 17 | expression *cronexpr.Expression 18 | cmd string 19 | timer *time.Timer 20 | } 21 | 22 | var filename string 23 | 24 | var todos []schedule 25 | var cases []reflect.SelectCase 26 | 27 | func scheduler() { 28 | filename = "crontab" 29 | loadSchedule(filename) 30 | 31 | } 32 | 33 | func looper() { 34 | 35 | watcher, err := fsnotify.NewWatcher() 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | defer watcher.Close() 40 | 41 | err = watcher.Add(filename) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | cases[len(cases)-1] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(watcher.Events)} 47 | 48 | for { 49 | 50 | chosen, value, _ := reflect.Select(cases) 51 | if chosen == len(cases)-1 { 52 | event := reflect.Value(value).Interface().(fsnotify.Event) 53 | 54 | if event.Op&fsnotify.Write == fsnotify.Write { 55 | log.Println("modified file:", event.Name) 56 | break 57 | } 58 | 59 | } else { 60 | sched := todos[chosen] 61 | fmt.Println(sched.cmd) 62 | nextTime := sched.expression.Next(time.Now()) 63 | duration := nextTime.Sub(time.Now()) 64 | sched.timer.Reset(duration) 65 | } 66 | 67 | } 68 | log.Println("reloaded schedule") 69 | loadSchedule(filename) 70 | } 71 | 72 | func loadSchedule(localname string) { 73 | 74 | reader, err := os.Open(localname) 75 | if err != nil { 76 | log.Printf("Could not open crontab file %s", err.Error()) 77 | return 78 | } 79 | todos = nil 80 | cases = nil 81 | 82 | scanner := bufio.NewScanner(reader) 83 | for scanner.Scan() { 84 | fmt.Println(scanner.Text()) // Println will add back the final '\n' 85 | fields := strings.Fields(scanner.Text()) 86 | cmd := fields[7] 87 | expr, err := cronexpr.Parse(scanner.Text()) 88 | if err != nil { 89 | log.Printf("crontab parser error: %q", err.Error()) 90 | continue 91 | 92 | } 93 | nextTime := expr.Next(time.Now()) 94 | duration := nextTime.Sub(time.Now()) 95 | timer := time.NewTimer(duration) 96 | sched := schedule{expression: expr, cmd: cmd, timer: timer} 97 | todos = append(todos, sched) 98 | 99 | } 100 | 101 | if err := scanner.Err(); err != nil { 102 | fmt.Fprintln(os.Stderr, "reading standard input:", err) 103 | return 104 | } 105 | 106 | cases = make([]reflect.SelectCase, len(todos)+1) 107 | for i, ch := range todos { 108 | cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch.timer.C)} 109 | } 110 | 111 | looper() 112 | } 113 | -------------------------------------------------------------------------------- /mhserver/view/cameras.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cameras 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

My Camera Monitoring

18 |
19 | 20 |
21 |
22 |

23 |
24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /mhserver/view/config/Hello.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"notification","Value":"Just saying hello"},"Targets":["hmi"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/New.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/connected.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"listdevices","Value":{"Type":"all"}},"Targets":["text"],"Type":"command"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/dirtest.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"oscommand","Value": {"cmd":"ls", "args":["-l", "-a", "-i"]}},"Targets":["camera"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/eastshoplightoff.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"mode","Value":"off"},"Targets":["eastshoplight"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/eastshoplighton.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"mode","Value":"on"},"Targets":["eastshoplight"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/lunch.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"notification","Value":"is it time for lunch?"},"Targets":["hmi"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/motiondetectoff.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"oscommand","Value":"setconf -k m -v -1"},"Targets":["camera"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/motiondetecton.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"oscommand","Value":"setconf -k m -v 4"},"Targets":["camera"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/mute.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"sound","Value":"off"},"Targets":["hmi"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/nightmodeoff.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"nightmode","Value":"off"},"Targets":["camera"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/nightmodeon.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"nightmode","Value":"on"},"Targets":["camera"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/northshoplightoff.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"mode","Value":"off"},"Targets":["northshoplight"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/northshoplighton.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"mode","Value":"on"},"Targets":["northshoplight"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/note.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"notification","Value":"Just saying hello"},"Targets":["hmi","text"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/restartclient.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"oscommand","Value": {"cmd":"/usr/scripts/restart-client.sh", "args":[]}},"Targets":["camera"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/setshopthermostat.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"set","Value":"75"},"Targets":["shopthermostat"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/southshoplightoff.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"mode","Value":"off"},"Targets":["southshoplight"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/southshoplighton.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Name":"mode","Value":"on"},"Targets":["southshoplight"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/unmute.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"sound","Value":"on"},"Targets":["hmi"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/config/volume-100.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"volume","Value":"100%"},"Targets":["hmi"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/volume-25.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"volume","Value":"25%"},"Targets":["hmi"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/volume-50.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"volume","Value":"50%"},"Targets":["hmi"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/volume-75.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"volume","Value":"75%"},"Targets":["hmi"],"Type":"control"} 2 | -------------------------------------------------------------------------------- /mhserver/view/config/volume.json: -------------------------------------------------------------------------------- 1 | {"Msg":{"Type":"volume","Value":"50%"},"Targets":["hmi"],"Type":"control"} -------------------------------------------------------------------------------- /mhserver/view/control.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Home 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

20 | My Home Control Panel 21 |

22 | 23 | 24 |
25 |
26 |

27 | Lights 28 |


29 | 30 | 31 |
32 | 33 |
34 |

35 | Cameras 36 |


37 | 38 | 39 |
40 | 41 |
42 |

43 | HMI 44 |


45 | 46 | 47 |
48 | 49 | 50 |
51 |

52 | Misc 53 |


54 | 55 | 56 |
57 |
58 | 59 |
60 | 61 |
62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /mhserver/view/css/bulma-switch.min.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.switch[type=checkbox]{outline:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:inline-block;position:absolute;opacity:0}.switch[type=checkbox]:focus+label::after,.switch[type=checkbox]:focus+label::before,.switch[type=checkbox]:focus+label:after,.switch[type=checkbox]:focus+label:before{outline:1px dotted #b5b5b5}.switch[type=checkbox][disabled]{cursor:not-allowed}.switch[type=checkbox][disabled]+label{opacity:.5}.switch[type=checkbox][disabled]+label::before,.switch[type=checkbox][disabled]+label:before{opacity:.5}.switch[type=checkbox][disabled]+label::after,.switch[type=checkbox][disabled]+label:after{opacity:.5}.switch[type=checkbox][disabled]+label:hover{cursor:not-allowed}.switch[type=checkbox]+label{position:relative;display:initial;font-size:1rem;line-height:initial;padding-left:3.5rem;padding-top:.2rem;cursor:pointer}.switch[type=checkbox]+label::before,.switch[type=checkbox]+label:before{position:absolute;display:block;top:0;left:0;width:3rem;height:1.5rem;border:.1rem solid transparent;border-radius:4px;background:#b5b5b5;content:''}.switch[type=checkbox]+label::after,.switch[type=checkbox]+label:after{display:block;position:absolute;top:.25rem;left:.25rem;width:1rem;height:1rem;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);border-radius:4px;background:#fff;-webkit-transition:all .25s ease-out;transition:all .25s ease-out;content:''}.switch[type=checkbox].is-rtl+label{padding-left:0;padding-right:3.5rem}.switch[type=checkbox].is-rtl+label::before,.switch[type=checkbox].is-rtl+label:before{left:auto;right:0}.switch[type=checkbox].is-rtl+label::after,.switch[type=checkbox].is-rtl+label:after{left:auto;right:1.625rem}.switch[type=checkbox]:checked+label::before,.switch[type=checkbox]:checked+label:before{background:#00d1b2}.switch[type=checkbox]:checked+label::after{left:1.625rem}.switch[type=checkbox]:checked.is-rtl+label::after,.switch[type=checkbox]:checked.is-rtl+label:after{left:auto;right:.25rem}.switch[type=checkbox].is-outlined+label::before,.switch[type=checkbox].is-outlined+label:before{background-color:transparent;border-color:#b5b5b5}.switch[type=checkbox].is-outlined+label::after,.switch[type=checkbox].is-outlined+label:after{background:#b5b5b5}.switch[type=checkbox].is-outlined:checked+label::before,.switch[type=checkbox].is-outlined:checked+label:before{background-color:transparent;border-color:#00d1b2}.switch[type=checkbox].is-outlined:checked+label::after,.switch[type=checkbox].is-outlined:checked+label:after{background:#00d1b2}.switch[type=checkbox].is-thin+label::before,.switch[type=checkbox].is-thin+label:before{top:.54545rem;height:.375rem}.switch[type=checkbox].is-thin+label::after,.switch[type=checkbox].is-thin+label:after{-webkit-box-shadow:0 0 3px #7a7a7a;box-shadow:0 0 3px #7a7a7a}.switch[type=checkbox].is-rounded+label::before,.switch[type=checkbox].is-rounded+label:before{border-radius:24px}.switch[type=checkbox].is-rounded+label::after,.switch[type=checkbox].is-rounded+label:after{border-radius:50%}.switch[type=checkbox].is-small+label{position:relative;display:initial;font-size:.75rem;line-height:initial;padding-left:2.75rem;padding-top:.2rem;cursor:pointer}.switch[type=checkbox].is-small+label::before,.switch[type=checkbox].is-small+label:before{position:absolute;display:block;top:0;left:0;width:2.25rem;height:1.125rem;border:.1rem solid transparent;border-radius:4px;background:#b5b5b5;content:''}.switch[type=checkbox].is-small+label::after,.switch[type=checkbox].is-small+label:after{display:block;position:absolute;top:.25rem;left:.25rem;width:.625rem;height:.625rem;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);border-radius:4px;background:#fff;-webkit-transition:all .25s ease-out;transition:all .25s ease-out;content:''}.switch[type=checkbox].is-small.is-rtl+label{padding-left:0;padding-right:2.75rem}.switch[type=checkbox].is-small.is-rtl+label::before,.switch[type=checkbox].is-small.is-rtl+label:before{left:auto;right:0}.switch[type=checkbox].is-small.is-rtl+label::after,.switch[type=checkbox].is-small.is-rtl+label:after{left:auto;right:1.25rem}.switch[type=checkbox].is-small:checked+label::before,.switch[type=checkbox].is-small:checked+label:before{background:#00d1b2}.switch[type=checkbox].is-small:checked+label::after{left:1.25rem}.switch[type=checkbox].is-small:checked.is-rtl+label::after,.switch[type=checkbox].is-small:checked.is-rtl+label:after{left:auto;right:.25rem}.switch[type=checkbox].is-small.is-outlined+label::before,.switch[type=checkbox].is-small.is-outlined+label:before{background-color:transparent;border-color:#b5b5b5}.switch[type=checkbox].is-small.is-outlined+label::after,.switch[type=checkbox].is-small.is-outlined+label:after{background:#b5b5b5}.switch[type=checkbox].is-small.is-outlined:checked+label::before,.switch[type=checkbox].is-small.is-outlined:checked+label:before{background-color:transparent;border-color:#00d1b2}.switch[type=checkbox].is-small.is-outlined:checked+label::after,.switch[type=checkbox].is-small.is-outlined:checked+label:after{background:#00d1b2}.switch[type=checkbox].is-small.is-thin+label::before,.switch[type=checkbox].is-small.is-thin+label:before{top:.40909rem;height:.28125rem}.switch[type=checkbox].is-small.is-thin+label::after,.switch[type=checkbox].is-small.is-thin+label:after{-webkit-box-shadow:0 0 3px #7a7a7a;box-shadow:0 0 3px #7a7a7a}.switch[type=checkbox].is-small.is-rounded+label::before,.switch[type=checkbox].is-small.is-rounded+label:before{border-radius:24px}.switch[type=checkbox].is-small.is-rounded+label::after,.switch[type=checkbox].is-small.is-rounded+label:after{border-radius:50%}.switch[type=checkbox].is-medium+label{position:relative;display:initial;font-size:1.25rem;line-height:initial;padding-left:4.25rem;padding-top:.2rem;cursor:pointer}.switch[type=checkbox].is-medium+label::before,.switch[type=checkbox].is-medium+label:before{position:absolute;display:block;top:0;left:0;width:3.75rem;height:1.875rem;border:.1rem solid transparent;border-radius:4px;background:#b5b5b5;content:''}.switch[type=checkbox].is-medium+label::after,.switch[type=checkbox].is-medium+label:after{display:block;position:absolute;top:.25rem;left:.25rem;width:1.375rem;height:1.375rem;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);border-radius:4px;background:#fff;-webkit-transition:all .25s ease-out;transition:all .25s ease-out;content:''}.switch[type=checkbox].is-medium.is-rtl+label{padding-left:0;padding-right:4.25rem}.switch[type=checkbox].is-medium.is-rtl+label::before,.switch[type=checkbox].is-medium.is-rtl+label:before{left:auto;right:0}.switch[type=checkbox].is-medium.is-rtl+label::after,.switch[type=checkbox].is-medium.is-rtl+label:after{left:auto;right:2rem}.switch[type=checkbox].is-medium:checked+label::before,.switch[type=checkbox].is-medium:checked+label:before{background:#00d1b2}.switch[type=checkbox].is-medium:checked+label::after{left:2rem}.switch[type=checkbox].is-medium:checked.is-rtl+label::after,.switch[type=checkbox].is-medium:checked.is-rtl+label:after{left:auto;right:.25rem}.switch[type=checkbox].is-medium.is-outlined+label::before,.switch[type=checkbox].is-medium.is-outlined+label:before{background-color:transparent;border-color:#b5b5b5}.switch[type=checkbox].is-medium.is-outlined+label::after,.switch[type=checkbox].is-medium.is-outlined+label:after{background:#b5b5b5}.switch[type=checkbox].is-medium.is-outlined:checked+label::before,.switch[type=checkbox].is-medium.is-outlined:checked+label:before{background-color:transparent;border-color:#00d1b2}.switch[type=checkbox].is-medium.is-outlined:checked+label::after,.switch[type=checkbox].is-medium.is-outlined:checked+label:after{background:#00d1b2}.switch[type=checkbox].is-medium.is-thin+label::before,.switch[type=checkbox].is-medium.is-thin+label:before{top:.68182rem;height:.46875rem}.switch[type=checkbox].is-medium.is-thin+label::after,.switch[type=checkbox].is-medium.is-thin+label:after{-webkit-box-shadow:0 0 3px #7a7a7a;box-shadow:0 0 3px #7a7a7a}.switch[type=checkbox].is-medium.is-rounded+label::before,.switch[type=checkbox].is-medium.is-rounded+label:before{border-radius:24px}.switch[type=checkbox].is-medium.is-rounded+label::after,.switch[type=checkbox].is-medium.is-rounded+label:after{border-radius:50%}.switch[type=checkbox].is-large+label{position:relative;display:initial;font-size:1.5rem;line-height:initial;padding-left:5rem;padding-top:.2rem;cursor:pointer}.switch[type=checkbox].is-large+label::before,.switch[type=checkbox].is-large+label:before{position:absolute;display:block;top:0;left:0;width:4.5rem;height:2.25rem;border:.1rem solid transparent;border-radius:4px;background:#b5b5b5;content:''}.switch[type=checkbox].is-large+label::after,.switch[type=checkbox].is-large+label:after{display:block;position:absolute;top:.25rem;left:.25rem;width:1.75rem;height:1.75rem;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);border-radius:4px;background:#fff;-webkit-transition:all .25s ease-out;transition:all .25s ease-out;content:''}.switch[type=checkbox].is-large.is-rtl+label{padding-left:0;padding-right:5rem}.switch[type=checkbox].is-large.is-rtl+label::before,.switch[type=checkbox].is-large.is-rtl+label:before{left:auto;right:0}.switch[type=checkbox].is-large.is-rtl+label::after,.switch[type=checkbox].is-large.is-rtl+label:after{left:auto;right:2.375rem}.switch[type=checkbox].is-large:checked+label::before,.switch[type=checkbox].is-large:checked+label:before{background:#00d1b2}.switch[type=checkbox].is-large:checked+label::after{left:2.375rem}.switch[type=checkbox].is-large:checked.is-rtl+label::after,.switch[type=checkbox].is-large:checked.is-rtl+label:after{left:auto;right:.25rem}.switch[type=checkbox].is-large.is-outlined+label::before,.switch[type=checkbox].is-large.is-outlined+label:before{background-color:transparent;border-color:#b5b5b5}.switch[type=checkbox].is-large.is-outlined+label::after,.switch[type=checkbox].is-large.is-outlined+label:after{background:#b5b5b5}.switch[type=checkbox].is-large.is-outlined:checked+label::before,.switch[type=checkbox].is-large.is-outlined:checked+label:before{background-color:transparent;border-color:#00d1b2}.switch[type=checkbox].is-large.is-outlined:checked+label::after,.switch[type=checkbox].is-large.is-outlined:checked+label:after{background:#00d1b2}.switch[type=checkbox].is-large.is-thin+label::before,.switch[type=checkbox].is-large.is-thin+label:before{top:.81818rem;height:.5625rem}.switch[type=checkbox].is-large.is-thin+label::after,.switch[type=checkbox].is-large.is-thin+label:after{-webkit-box-shadow:0 0 3px #7a7a7a;box-shadow:0 0 3px #7a7a7a}.switch[type=checkbox].is-large.is-rounded+label::before,.switch[type=checkbox].is-large.is-rounded+label:before{border-radius:24px}.switch[type=checkbox].is-large.is-rounded+label::after,.switch[type=checkbox].is-large.is-rounded+label:after{border-radius:50%}.switch[type=checkbox].is-white:checked+label::before,.switch[type=checkbox].is-white:checked+label:before{background:#fff}.switch[type=checkbox].is-white.is-outlined:checked+label::before,.switch[type=checkbox].is-white.is-outlined:checked+label:before{background-color:transparent;border-color:#fff!important}.switch[type=checkbox].is-white.is-outlined:checked+label::after,.switch[type=checkbox].is-white.is-outlined:checked+label:after{background:#fff}.switch[type=checkbox].is-white.is-thin.is-outlined+label::after,.switch[type=checkbox].is-white.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-white+label::before,.switch[type=checkbox].is-unchecked-white+label:before{background:#fff}.switch[type=checkbox].is-unchecked-white.is-outlined+label::before,.switch[type=checkbox].is-unchecked-white.is-outlined+label:before{background-color:transparent;border-color:#fff!important}.switch[type=checkbox].is-unchecked-white.is-outlined+label::after,.switch[type=checkbox].is-unchecked-white.is-outlined+label:after{background:#fff}.switch[type=checkbox].is-black:checked+label::before,.switch[type=checkbox].is-black:checked+label:before{background:#0a0a0a}.switch[type=checkbox].is-black.is-outlined:checked+label::before,.switch[type=checkbox].is-black.is-outlined:checked+label:before{background-color:transparent;border-color:#0a0a0a!important}.switch[type=checkbox].is-black.is-outlined:checked+label::after,.switch[type=checkbox].is-black.is-outlined:checked+label:after{background:#0a0a0a}.switch[type=checkbox].is-black.is-thin.is-outlined+label::after,.switch[type=checkbox].is-black.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-black+label::before,.switch[type=checkbox].is-unchecked-black+label:before{background:#0a0a0a}.switch[type=checkbox].is-unchecked-black.is-outlined+label::before,.switch[type=checkbox].is-unchecked-black.is-outlined+label:before{background-color:transparent;border-color:#0a0a0a!important}.switch[type=checkbox].is-unchecked-black.is-outlined+label::after,.switch[type=checkbox].is-unchecked-black.is-outlined+label:after{background:#0a0a0a}.switch[type=checkbox].is-light:checked+label::before,.switch[type=checkbox].is-light:checked+label:before{background:#f5f5f5}.switch[type=checkbox].is-light.is-outlined:checked+label::before,.switch[type=checkbox].is-light.is-outlined:checked+label:before{background-color:transparent;border-color:#f5f5f5!important}.switch[type=checkbox].is-light.is-outlined:checked+label::after,.switch[type=checkbox].is-light.is-outlined:checked+label:after{background:#f5f5f5}.switch[type=checkbox].is-light.is-thin.is-outlined+label::after,.switch[type=checkbox].is-light.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-light+label::before,.switch[type=checkbox].is-unchecked-light+label:before{background:#f5f5f5}.switch[type=checkbox].is-unchecked-light.is-outlined+label::before,.switch[type=checkbox].is-unchecked-light.is-outlined+label:before{background-color:transparent;border-color:#f5f5f5!important}.switch[type=checkbox].is-unchecked-light.is-outlined+label::after,.switch[type=checkbox].is-unchecked-light.is-outlined+label:after{background:#f5f5f5}.switch[type=checkbox].is-dark:checked+label::before,.switch[type=checkbox].is-dark:checked+label:before{background:#363636}.switch[type=checkbox].is-dark.is-outlined:checked+label::before,.switch[type=checkbox].is-dark.is-outlined:checked+label:before{background-color:transparent;border-color:#363636!important}.switch[type=checkbox].is-dark.is-outlined:checked+label::after,.switch[type=checkbox].is-dark.is-outlined:checked+label:after{background:#363636}.switch[type=checkbox].is-dark.is-thin.is-outlined+label::after,.switch[type=checkbox].is-dark.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-dark+label::before,.switch[type=checkbox].is-unchecked-dark+label:before{background:#363636}.switch[type=checkbox].is-unchecked-dark.is-outlined+label::before,.switch[type=checkbox].is-unchecked-dark.is-outlined+label:before{background-color:transparent;border-color:#363636!important}.switch[type=checkbox].is-unchecked-dark.is-outlined+label::after,.switch[type=checkbox].is-unchecked-dark.is-outlined+label:after{background:#363636}.switch[type=checkbox].is-primary:checked+label::before,.switch[type=checkbox].is-primary:checked+label:before{background:#00d1b2}.switch[type=checkbox].is-primary.is-outlined:checked+label::before,.switch[type=checkbox].is-primary.is-outlined:checked+label:before{background-color:transparent;border-color:#00d1b2!important}.switch[type=checkbox].is-primary.is-outlined:checked+label::after,.switch[type=checkbox].is-primary.is-outlined:checked+label:after{background:#00d1b2}.switch[type=checkbox].is-primary.is-thin.is-outlined+label::after,.switch[type=checkbox].is-primary.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-primary+label::before,.switch[type=checkbox].is-unchecked-primary+label:before{background:#00d1b2}.switch[type=checkbox].is-unchecked-primary.is-outlined+label::before,.switch[type=checkbox].is-unchecked-primary.is-outlined+label:before{background-color:transparent;border-color:#00d1b2!important}.switch[type=checkbox].is-unchecked-primary.is-outlined+label::after,.switch[type=checkbox].is-unchecked-primary.is-outlined+label:after{background:#00d1b2}.switch[type=checkbox].is-link:checked+label::before,.switch[type=checkbox].is-link:checked+label:before{background:#3273dc}.switch[type=checkbox].is-link.is-outlined:checked+label::before,.switch[type=checkbox].is-link.is-outlined:checked+label:before{background-color:transparent;border-color:#3273dc!important}.switch[type=checkbox].is-link.is-outlined:checked+label::after,.switch[type=checkbox].is-link.is-outlined:checked+label:after{background:#3273dc}.switch[type=checkbox].is-link.is-thin.is-outlined+label::after,.switch[type=checkbox].is-link.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-link+label::before,.switch[type=checkbox].is-unchecked-link+label:before{background:#3273dc}.switch[type=checkbox].is-unchecked-link.is-outlined+label::before,.switch[type=checkbox].is-unchecked-link.is-outlined+label:before{background-color:transparent;border-color:#3273dc!important}.switch[type=checkbox].is-unchecked-link.is-outlined+label::after,.switch[type=checkbox].is-unchecked-link.is-outlined+label:after{background:#3273dc}.switch[type=checkbox].is-info:checked+label::before,.switch[type=checkbox].is-info:checked+label:before{background:#209cee}.switch[type=checkbox].is-info.is-outlined:checked+label::before,.switch[type=checkbox].is-info.is-outlined:checked+label:before{background-color:transparent;border-color:#209cee!important}.switch[type=checkbox].is-info.is-outlined:checked+label::after,.switch[type=checkbox].is-info.is-outlined:checked+label:after{background:#209cee}.switch[type=checkbox].is-info.is-thin.is-outlined+label::after,.switch[type=checkbox].is-info.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-info+label::before,.switch[type=checkbox].is-unchecked-info+label:before{background:#209cee}.switch[type=checkbox].is-unchecked-info.is-outlined+label::before,.switch[type=checkbox].is-unchecked-info.is-outlined+label:before{background-color:transparent;border-color:#209cee!important}.switch[type=checkbox].is-unchecked-info.is-outlined+label::after,.switch[type=checkbox].is-unchecked-info.is-outlined+label:after{background:#209cee}.switch[type=checkbox].is-success:checked+label::before,.switch[type=checkbox].is-success:checked+label:before{background:#23d160}.switch[type=checkbox].is-success.is-outlined:checked+label::before,.switch[type=checkbox].is-success.is-outlined:checked+label:before{background-color:transparent;border-color:#23d160!important}.switch[type=checkbox].is-success.is-outlined:checked+label::after,.switch[type=checkbox].is-success.is-outlined:checked+label:after{background:#23d160}.switch[type=checkbox].is-success.is-thin.is-outlined+label::after,.switch[type=checkbox].is-success.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-success+label::before,.switch[type=checkbox].is-unchecked-success+label:before{background:#23d160}.switch[type=checkbox].is-unchecked-success.is-outlined+label::before,.switch[type=checkbox].is-unchecked-success.is-outlined+label:before{background-color:transparent;border-color:#23d160!important}.switch[type=checkbox].is-unchecked-success.is-outlined+label::after,.switch[type=checkbox].is-unchecked-success.is-outlined+label:after{background:#23d160}.switch[type=checkbox].is-warning:checked+label::before,.switch[type=checkbox].is-warning:checked+label:before{background:#ffdd57}.switch[type=checkbox].is-warning.is-outlined:checked+label::before,.switch[type=checkbox].is-warning.is-outlined:checked+label:before{background-color:transparent;border-color:#ffdd57!important}.switch[type=checkbox].is-warning.is-outlined:checked+label::after,.switch[type=checkbox].is-warning.is-outlined:checked+label:after{background:#ffdd57}.switch[type=checkbox].is-warning.is-thin.is-outlined+label::after,.switch[type=checkbox].is-warning.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-warning+label::before,.switch[type=checkbox].is-unchecked-warning+label:before{background:#ffdd57}.switch[type=checkbox].is-unchecked-warning.is-outlined+label::before,.switch[type=checkbox].is-unchecked-warning.is-outlined+label:before{background-color:transparent;border-color:#ffdd57!important}.switch[type=checkbox].is-unchecked-warning.is-outlined+label::after,.switch[type=checkbox].is-unchecked-warning.is-outlined+label:after{background:#ffdd57}.switch[type=checkbox].is-danger:checked+label::before,.switch[type=checkbox].is-danger:checked+label:before{background:#ff3860}.switch[type=checkbox].is-danger.is-outlined:checked+label::before,.switch[type=checkbox].is-danger.is-outlined:checked+label:before{background-color:transparent;border-color:#ff3860!important}.switch[type=checkbox].is-danger.is-outlined:checked+label::after,.switch[type=checkbox].is-danger.is-outlined:checked+label:after{background:#ff3860}.switch[type=checkbox].is-danger.is-thin.is-outlined+label::after,.switch[type=checkbox].is-danger.is-thin.is-outlined+label:after{-webkit-box-shadow:none;box-shadow:none}.switch[type=checkbox].is-unchecked-danger+label::before,.switch[type=checkbox].is-unchecked-danger+label:before{background:#ff3860}.switch[type=checkbox].is-unchecked-danger.is-outlined+label::before,.switch[type=checkbox].is-unchecked-danger.is-outlined+label:before{background-color:transparent;border-color:#ff3860!important}.switch[type=checkbox].is-unchecked-danger.is-outlined+label::after,.switch[type=checkbox].is-unchecked-danger.is-outlined+label:after{background:#ff3860} 2 | -------------------------------------------------------------------------------- /mhserver/view/css/img/jsoneditor-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machinesworking/myhome/f587956724c2c14a66511f97694a69508b8b5651/mhserver/view/css/img/jsoneditor-icons.png -------------------------------------------------------------------------------- /mhserver/view/css/jsoneditor.css: -------------------------------------------------------------------------------- 1 | .jsoneditor .field, 2 | .jsoneditor .value, 3 | .jsoneditor .readonly { 4 | border: 1px solid transparent; 5 | min-height: 16px; 6 | min-width: 32px; 7 | padding: 2px; 8 | margin: 1px; 9 | word-wrap: break-word; 10 | float: left; 11 | } 12 | 13 | /* adjust margin of p elements inside editable divs, needed for Opera, IE */ 14 | 15 | .jsoneditor .field p, 16 | .jsoneditor .value p { 17 | margin: 0; 18 | } 19 | 20 | .jsoneditor .value { 21 | word-break: break-word; 22 | } 23 | 24 | .jsoneditor .readonly { 25 | min-width: 16px; 26 | color: gray; 27 | } 28 | 29 | .jsoneditor .empty { 30 | border-color: lightgray; 31 | border-style: dashed; 32 | border-radius: 2px; 33 | } 34 | 35 | .jsoneditor .field.empty { 36 | background-image: url("img/jsoneditor-icons.png"); 37 | background-position: 0 -144px; 38 | } 39 | 40 | .jsoneditor .value.empty { 41 | background-image: url("img/jsoneditor-icons.png"); 42 | background-position: -48px -144px; 43 | } 44 | 45 | .jsoneditor .value.url { 46 | color: green; 47 | text-decoration: underline; 48 | } 49 | 50 | .jsoneditor a.value.url:hover, 51 | .jsoneditor a.value.url:focus { 52 | color: red; 53 | } 54 | 55 | .jsoneditor .separator { 56 | padding: 3px 0; 57 | vertical-align: top; 58 | color: gray; 59 | } 60 | 61 | .jsoneditor .field[contenteditable=true]:focus, 62 | .jsoneditor .field[contenteditable=true]:hover, 63 | .jsoneditor .value[contenteditable=true]:focus, 64 | .jsoneditor .value[contenteditable=true]:hover, 65 | .jsoneditor .field.highlight, 66 | .jsoneditor .value.highlight { 67 | background-color: #FFFFAB; 68 | border: 1px solid yellow; 69 | border-radius: 2px; 70 | } 71 | 72 | .jsoneditor .field.highlight-active, 73 | .jsoneditor .field.highlight-active:focus, 74 | .jsoneditor .field.highlight-active:hover, 75 | .jsoneditor .value.highlight-active, 76 | .jsoneditor .value.highlight-active:focus, 77 | .jsoneditor .value.highlight-active:hover { 78 | background-color: #ffee00; 79 | border: 1px solid #ffc700; 80 | border-radius: 2px; 81 | } 82 | 83 | .jsoneditor div.tree button { 84 | width: 24px; 85 | height: 24px; 86 | padding: 0; 87 | margin: 0; 88 | border: none; 89 | cursor: pointer; 90 | background: transparent url("img/jsoneditor-icons.png"); 91 | } 92 | 93 | .jsoneditor div.tree button.collapsed { 94 | background-position: 0 -48px; 95 | } 96 | 97 | .jsoneditor div.tree button.expanded { 98 | background-position: 0 -72px; 99 | } 100 | 101 | .jsoneditor div.tree button.contextmenu { 102 | background-position: -48px -72px; 103 | } 104 | 105 | .jsoneditor div.tree button.contextmenu:hover, 106 | .jsoneditor div.tree button.contextmenu:focus, 107 | .jsoneditor div.tree button.contextmenu.selected { 108 | background-position: -48px -48px; 109 | } 110 | 111 | .jsoneditor div.tree *:focus { 112 | outline: none; 113 | } 114 | 115 | .jsoneditor div.tree button:focus { 116 | /* TODO: nice outline for buttons with focus 117 | outline: #97B0F8 solid 2px; 118 | box-shadow: 0 0 8px #97B0F8; 119 | */ 120 | background-color: #f5f5f5; 121 | outline: #e5e5e5 solid 1px; 122 | } 123 | 124 | .jsoneditor div.tree button.invisible { 125 | visibility: hidden; 126 | background: none; 127 | } 128 | 129 | .jsoneditor { 130 | color: #1A1A1A; 131 | border: 1px solid #97B0F8; 132 | -moz-box-sizing: border-box; 133 | -webkit-box-sizing: border-box; 134 | box-sizing: border-box; 135 | width: 100%; 136 | height: 100%; 137 | overflow: auto; 138 | position: relative; 139 | padding: 0; 140 | line-height: 100%; 141 | } 142 | 143 | .jsoneditor div.tree table.tree { 144 | border-collapse: collapse; 145 | border-spacing: 0; 146 | width: 100%; 147 | margin: 0; 148 | } 149 | 150 | .jsoneditor div.outer { 151 | width: 100%; 152 | height: 100%; 153 | margin: -35px 0 0 0; 154 | padding: 35px 0 0 0; 155 | -moz-box-sizing: border-box; 156 | -webkit-box-sizing: border-box; 157 | box-sizing: border-box; 158 | overflow: hidden; 159 | } 160 | 161 | .jsoneditor div.tree { 162 | width: 100%; 163 | height: 100%; 164 | position: relative; 165 | overflow: auto; 166 | } 167 | 168 | .jsoneditor textarea.text { 169 | width: 100%; 170 | height: 100%; 171 | margin: 0; 172 | -moz-box-sizing: border-box; 173 | -webkit-box-sizing: border-box; 174 | box-sizing: border-box; 175 | border: none; 176 | background-color: white; 177 | resize: none; 178 | } 179 | 180 | .jsoneditor tr.highlight { 181 | background-color: #FFFFAB; 182 | } 183 | 184 | .jsoneditor div.tree button.dragarea { 185 | background: url("img/jsoneditor-icons.png") -72px -72px; 186 | cursor: move; 187 | } 188 | 189 | .jsoneditor div.tree button.dragarea:hover, 190 | .jsoneditor div.tree button.dragarea:focus { 191 | background-position: -72px -48px; 192 | } 193 | 194 | .jsoneditor tr, 195 | .jsoneditor th, 196 | .jsoneditor td { 197 | padding: 0; 198 | margin: 0; 199 | } 200 | 201 | .jsoneditor td { 202 | vertical-align: top; 203 | } 204 | 205 | .jsoneditor td.tree { 206 | vertical-align: top; 207 | } 208 | 209 | .jsoneditor .field, 210 | .jsoneditor .value, 211 | .jsoneditor td, 212 | .jsoneditor th, 213 | .jsoneditor textarea { 214 | font-family: droid sans mono, consolas, monospace, courier new, courier, sans-serif; 215 | font-size: 10pt; 216 | color: #1A1A1A; 217 | } 218 | /* ContextMenu - main menu */ 219 | 220 | .jsoneditor-contextmenu { 221 | position: absolute; 222 | z-index: 99999; 223 | } 224 | 225 | .jsoneditor-contextmenu ul { 226 | position: relative; 227 | left: 0; 228 | top: 0; 229 | width: 124px; 230 | background: white; 231 | border: 1px solid #d3d3d3; 232 | box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3); 233 | list-style: none; 234 | margin: 0; 235 | padding: 0; 236 | } 237 | 238 | .jsoneditor-contextmenu ul li button { 239 | padding: 0; 240 | margin: 0; 241 | width: 124px; 242 | height: 24px; 243 | border: none; 244 | cursor: pointer; 245 | color: #4d4d4d; 246 | background: transparent; 247 | line-height: 26px; 248 | text-align: left; 249 | } 250 | 251 | /* Fix button padding in firefox */ 252 | 253 | .jsoneditor-contextmenu ul li button::-moz-focus-inner { 254 | padding: 0; 255 | border: 0; 256 | } 257 | 258 | .jsoneditor-contextmenu ul li button:hover, 259 | .jsoneditor-contextmenu ul li button:focus { 260 | color: #1a1a1a; 261 | background-color: #f5f5f5; 262 | outline: none; 263 | } 264 | 265 | .jsoneditor-contextmenu ul li button.default { 266 | width: 92px; 267 | } 268 | 269 | .jsoneditor-contextmenu ul li button.expand { 270 | float: right; 271 | width: 32px; 272 | height: 24px; 273 | border-left: 1px solid #e5e5e5; 274 | } 275 | 276 | .jsoneditor-contextmenu div.icon { 277 | float: left; 278 | width: 24px; 279 | height: 24px; 280 | border: none; 281 | padding: 0; 282 | margin: 0; 283 | background-image: url("img/jsoneditor-icons.png"); 284 | } 285 | 286 | .jsoneditor-contextmenu ul li button div.expand { 287 | float: right; 288 | width: 24px; 289 | height: 24px; 290 | padding: 0; 291 | margin: 0 4px 0 0; 292 | background: url("img/jsoneditor-icons.png") 0 -72px; 293 | opacity: 0.4; 294 | } 295 | 296 | .jsoneditor-contextmenu ul li button:hover div.expand, 297 | .jsoneditor-contextmenu ul li button:focus div.expand, 298 | .jsoneditor-contextmenu ul li.selected div.expand, 299 | .jsoneditor-contextmenu ul li button.expand:hover div.expand, 300 | .jsoneditor-contextmenu ul li button.expand:focus div.expand { 301 | opacity: 1; 302 | } 303 | 304 | .jsoneditor-contextmenu .separator { 305 | height: 0; 306 | border-top: 1px solid #e5e5e5; 307 | padding-top: 5px; 308 | margin-top: 5px; 309 | } 310 | 311 | .jsoneditor-contextmenu button.remove > .icon { 312 | background-position: -24px -24px; 313 | } 314 | 315 | .jsoneditor-contextmenu button.remove:hover > .icon, 316 | .jsoneditor-contextmenu button.remove:focus > .icon { 317 | background-position: -24px 0; 318 | } 319 | 320 | .jsoneditor-contextmenu button.append > .icon { 321 | background-position: 0 -24px; 322 | } 323 | 324 | .jsoneditor-contextmenu button.append:hover > .icon, 325 | .jsoneditor-contextmenu button.append:focus > .icon { 326 | background-position: 0 0; 327 | } 328 | 329 | .jsoneditor-contextmenu button.insert > .icon { 330 | background-position: 0 -24px; 331 | } 332 | 333 | .jsoneditor-contextmenu button.insert:hover > .icon, 334 | .jsoneditor-contextmenu button.insert:focus > .icon { 335 | background-position: 0 0; 336 | } 337 | 338 | .jsoneditor-contextmenu button.duplicate > .icon { 339 | background-position: -48px -24px; 340 | } 341 | 342 | .jsoneditor-contextmenu button.duplicate:hover > .icon, 343 | .jsoneditor-contextmenu button.duplicate:focus > .icon { 344 | background-position: -48px 0; 345 | } 346 | 347 | .jsoneditor-contextmenu button.sort-asc > .icon { 348 | background-position: -168px -24px; 349 | } 350 | 351 | .jsoneditor-contextmenu button.sort-asc:hover > .icon, 352 | .jsoneditor-contextmenu button.sort-asc:focus > .icon { 353 | background-position: -168px 0; 354 | } 355 | 356 | .jsoneditor-contextmenu button.sort-desc > .icon { 357 | background-position: -192px -24px; 358 | } 359 | 360 | .jsoneditor-contextmenu button.sort-desc:hover > .icon, 361 | .jsoneditor-contextmenu button.sort-desc:focus > .icon { 362 | background-position: -192px 0; 363 | } 364 | 365 | /* ContextMenu - sub menu */ 366 | 367 | .jsoneditor-contextmenu ul li .selected { 368 | background-color: #D5DDF6; 369 | } 370 | 371 | .jsoneditor-contextmenu ul li { 372 | overflow: hidden; 373 | } 374 | 375 | .jsoneditor-contextmenu ul li ul { 376 | display: none; 377 | position: relative; 378 | left: -10px; 379 | top: 0; 380 | border: none; 381 | box-shadow: inset 0 0 10px rgba(128, 128, 128, 0.5); 382 | padding: 0 10px; 383 | /* TODO: transition is not supported on IE8-9 */ 384 | -webkit-transition: all 0.3s ease-out; 385 | -moz-transition: all 0.3s ease-out; 386 | -o-transition: all 0.3s ease-out; 387 | transition: all 0.3s ease-out; 388 | } 389 | 390 | 391 | 392 | .jsoneditor-contextmenu ul li ul li button { 393 | padding-left: 24px; 394 | } 395 | 396 | .jsoneditor-contextmenu ul li ul li button:hover, 397 | .jsoneditor-contextmenu ul li ul li button:focus { 398 | background-color: #f5f5f5; 399 | } 400 | 401 | .jsoneditor-contextmenu button.type-string > .icon { 402 | background-position: -144px -24px; 403 | } 404 | 405 | .jsoneditor-contextmenu button.type-string:hover > .icon, 406 | .jsoneditor-contextmenu button.type-string:focus > .icon, 407 | .jsoneditor-contextmenu button.type-string.selected > .icon { 408 | background-position: -144px 0; 409 | } 410 | 411 | .jsoneditor-contextmenu button.type-auto > .icon { 412 | background-position: -120px -24px; 413 | } 414 | 415 | .jsoneditor-contextmenu button.type-auto:hover > .icon, 416 | .jsoneditor-contextmenu button.type-auto:focus > .icon, 417 | .jsoneditor-contextmenu button.type-auto.selected > .icon { 418 | background-position: -120px 0; 419 | } 420 | 421 | .jsoneditor-contextmenu button.type-object > .icon { 422 | background-position: -72px -24px; 423 | } 424 | 425 | .jsoneditor-contextmenu button.type-object:hover > .icon, 426 | .jsoneditor-contextmenu button.type-object:focus > .icon, 427 | .jsoneditor-contextmenu button.type-object.selected > .icon { 428 | background-position: -72px 0; 429 | } 430 | 431 | .jsoneditor-contextmenu button.type-array > .icon { 432 | background-position: -96px -24px; 433 | } 434 | 435 | .jsoneditor-contextmenu button.type-array:hover > .icon, 436 | .jsoneditor-contextmenu button.type-array:focus > .icon, 437 | .jsoneditor-contextmenu button.type-array.selected > .icon { 438 | background-position: -96px 0; 439 | } 440 | 441 | .jsoneditor-contextmenu button.type-modes > .icon { 442 | background-image: none; 443 | width: 6px; 444 | } 445 | .jsoneditor .menu { 446 | width: 100%; 447 | height: 35px; 448 | padding: 2px; 449 | margin: 0; 450 | overflow: hidden; 451 | -moz-box-sizing: border-box; 452 | -webkit-box-sizing: border-box; 453 | box-sizing: border-box; 454 | color: #1A1A1A; 455 | background-color: #D5DDF6; 456 | border-bottom: 1px solid #97B0F8; 457 | } 458 | 459 | .jsoneditor .menu button { 460 | width: 26px; 461 | height: 26px; 462 | margin: 2px; 463 | padding: 0; 464 | border-radius: 2px; 465 | border: 1px solid #aec0f8; 466 | background: #e3eaf6 url("img/jsoneditor-icons.png"); 467 | color: #4D4D4D; 468 | opacity: 0.8; 469 | font-family: arial, sans-serif; 470 | font-size: 10pt; 471 | float: left; 472 | } 473 | 474 | .jsoneditor .menu button:hover { 475 | background-color: #f0f2f5; 476 | } 477 | 478 | .jsoneditor .menu button:focus, 479 | .jsoneditor .menu button:active { 480 | background-color: #ffffff; 481 | } 482 | 483 | .jsoneditor .menu button:disabled { 484 | background-color: #e3eaf6; 485 | } 486 | 487 | .jsoneditor .menu button.collapse-all { 488 | background-position: 0 -96px; 489 | } 490 | 491 | .jsoneditor .menu button.expand-all { 492 | background-position: 0 -120px; 493 | } 494 | 495 | .jsoneditor .menu button.undo { 496 | background-position: -24px -96px; 497 | } 498 | 499 | .jsoneditor .menu button.undo:disabled { 500 | background-position: -24px -120px; 501 | } 502 | 503 | .jsoneditor .menu button.redo { 504 | background-position: -48px -96px; 505 | } 506 | 507 | .jsoneditor .menu button.redo:disabled { 508 | background-position: -48px -120px; 509 | } 510 | 511 | .jsoneditor .menu button.compact { 512 | background-position: -72px -96px; 513 | } 514 | 515 | .jsoneditor .menu button.format { 516 | background-position: -72px -120px; 517 | } 518 | 519 | .jsoneditor .menu button.modes { 520 | background-image: none; 521 | width: auto; 522 | padding-left: 6px; 523 | padding-right: 6px; 524 | } 525 | 526 | .jsoneditor .menu button.separator { 527 | margin-left: 10px; 528 | } 529 | 530 | .jsoneditor .menu a { 531 | font-family: arial, sans-serif; 532 | font-size: 10pt; 533 | color: #97B0F8; 534 | vertical-align: middle; 535 | } 536 | 537 | .jsoneditor .menu a:hover { 538 | color: red; 539 | } 540 | 541 | .jsoneditor .menu a.poweredBy { 542 | font-size: 8pt; 543 | position: absolute; 544 | right: 0; 545 | top: 0; 546 | padding: 10px; 547 | } 548 | 549 | /* TODO: css for button:disabled is not supported by IE8 */ 550 | .jsoneditor .search input, 551 | .jsoneditor .search .results { 552 | font-family: arial, sans-serif; 553 | font-size: 10pt; 554 | color: #1A1A1A; 555 | background: transparent; 556 | /* For Firefox */ 557 | } 558 | 559 | .jsoneditor .search { 560 | position: absolute; 561 | right: 2px; 562 | top: 2px; 563 | } 564 | 565 | .jsoneditor .search .frame { 566 | border: 1px solid #97B0F8; 567 | background-color: white; 568 | padding: 0 2px; 569 | margin: 0; 570 | } 571 | 572 | .jsoneditor .search .frame table { 573 | border-collapse: collapse; 574 | } 575 | 576 | .jsoneditor .search input { 577 | width: 120px; 578 | border: none; 579 | outline: none; 580 | margin: 1px; 581 | } 582 | 583 | .jsoneditor .search .results { 584 | color: #4d4d4d; 585 | padding-right: 5px; 586 | line-height: 24px; 587 | } 588 | 589 | .jsoneditor .search button { 590 | width: 16px; 591 | height: 24px; 592 | padding: 0; 593 | margin: 0; 594 | border: none; 595 | background: url("img/jsoneditor-icons.png"); 596 | vertical-align: top; 597 | } 598 | 599 | .jsoneditor .search button:hover { 600 | background-color: transparent; 601 | } 602 | 603 | .jsoneditor .search button.refresh { 604 | width: 18px; 605 | background-position: -99px -73px; 606 | } 607 | 608 | .jsoneditor .search button.next { 609 | cursor: pointer; 610 | background-position: -124px -73px; 611 | } 612 | 613 | .jsoneditor .search button.next:hover { 614 | background-position: -124px -49px; 615 | } 616 | 617 | .jsoneditor .search button.previous { 618 | cursor: pointer; 619 | background-position: -148px -73px; 620 | margin-right: 2px; 621 | } 622 | 623 | .jsoneditor .search button.previous:hover { 624 | background-position: -148px -49px; 625 | } -------------------------------------------------------------------------------- /mhserver/view/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | .switch { 3 | position: relative; 4 | display: inline-block; 5 | width: 60px; 6 | height: 34px; 7 | } 8 | 9 | .switch input { 10 | opacity: 0; 11 | width: 0; 12 | height: 0; 13 | } 14 | 15 | .slider { 16 | position: absolute; 17 | cursor: pointer; 18 | top: 0; 19 | left: 0; 20 | right: 0; 21 | bottom: 0; 22 | background-color: #ccc; 23 | -webkit-transition: .4s; 24 | transition: .4s; 25 | } 26 | 27 | .slider:before { 28 | position: absolute; 29 | content: ""; 30 | height: 26px; 31 | width: 26px; 32 | left: 4px; 33 | bottom: 4px; 34 | background-color: white; 35 | -webkit-transition: .4s; 36 | transition: .4s; 37 | } 38 | 39 | input:checked + .slider { 40 | background-color: #2196F3; 41 | } 42 | 43 | input:focus + .slider { 44 | box-shadow: 0 0 1px #2196F3; 45 | } 46 | 47 | input:checked + .slider:before { 48 | -webkit-transform: translateX(26px); 49 | -ms-transform: translateX(26px); 50 | transform: translateX(26px); 51 | } 52 | 53 | /* Rounded sliders */ 54 | .slider.round { 55 | border-radius: 34px; 56 | } 57 | 58 | .slider.round:before { 59 | border-radius: 50%; 60 | } 61 | 62 | 63 | 64 | 65 | .seqconfig { 66 | position: absolute; 67 | right: 10px; 68 | top: 5px; 69 | width: 600px; 70 | bottom: 5px; 71 | } 72 | .seqmenu { 73 | position: absolute; 74 | left: 5px; 75 | top: 5px; 76 | right: 650px; 77 | bottom: 5px; 78 | 79 | } 80 | #headerdiv{ 81 | width: 100% 82 | } 83 | .seqcontainer{ 84 | position: relative; 85 | height: 500px; 86 | display: none; 87 | } 88 | 89 | #remoteVideos{ 90 | width:1300px; 91 | overflow: auto; 92 | 93 | } 94 | 95 | #statusDiv{ 96 | width:800px; 97 | Height:500px; 98 | overflow: auto; 99 | } 100 | #msgtext{ 101 | visibility: hidden 102 | } 103 | #saveButton, #loadButton, #selectButton{ 104 | background-color: #ccc; 105 | -moz-border-radius: 5px; 106 | -webkit-border-radius: 5px; 107 | border-radius:6px; 108 | color: #fff; 109 | } 110 | 111 | 112 | 113 | 114 | #shutdownDlgButton{ 115 | background-color: #df6666; 116 | width: 300px; 117 | height: 300px; 118 | -moz-border-radius: 150px; 119 | -webkit-border-radius: 150px; 120 | border-radius:150px; 121 | color: #fff; 122 | } 123 | #continueShutdownButton{ 124 | background-color: #ccc; 125 | width: 250px; 126 | height: 250px; 127 | -moz-border-radius: 125px; 128 | -webkit-border-radius: 125px; 129 | border-radius:125px; 130 | color: #fff; 131 | } 132 | 133 | #cancelShutdownButton{ 134 | background-color: #ccc; 135 | width: 250px; 136 | height: 250px; 137 | -moz-border-radius: 125px; 138 | -webkit-border-radius: 125px; 139 | border-radius:125px; 140 | color: #000000; 141 | } 142 | 143 | #shutdownDlgButton:hover , #continueShutdownButton:hover, #abortShutdownButton:hover{ 144 | border: none; 145 | background:red; 146 | box-shadow: 0px 0px 1px #777; 147 | 148 | 149 | } 150 | #cancelShutdownButton:hover{ 151 | border: none; 152 | background:yellow; 153 | box-shadow: 0px 0px 1px #777; 154 | 155 | 156 | } 157 | 158 | #saveButton:hover, #loadButton:hover, #selectButton:hover { 159 | border: none; 160 | background:blue; 161 | box-shadow: 0px 0px 1px #777; 162 | 163 | 164 | } 165 | 166 | #abortButton{ 167 | background-color: #ccc; 168 | width: 100px; 169 | height: 100px; 170 | -moz-border-radius: 50px; 171 | -webkit-border-radius: 50px; 172 | border-radius:50px; 173 | color: #000000; 174 | } 175 | 176 | 177 | body { 178 | font: 10.5pt arial; 179 | color: #4d4d4d; 180 | line-height: 150%; 181 | width: 800px; 182 | } 183 | 184 | code { 185 | background-color: #f5f5f5; 186 | } 187 | 188 | #jsoneditor { 189 | width: 600px; 190 | height: 500px; 191 | } 192 | 193 | 194 | 195 | .overlay { 196 | display: none; 197 | position: fixed; 198 | top: 0; 199 | left: 0; 200 | height: 100%; 201 | width: 100%; 202 | background-color: rgba(0,0,0,0.3); 203 | z-index: 10; 204 | } 205 | #abortDialog { 206 | display: none; 207 | opacity: 0.7; 208 | width: 120px; 209 | height: 100px; 210 | left: 660px; 211 | position: fixed; 212 | background-color: #bfbfbf; 213 | border-radius: 5px; 214 | text-align: center; 215 | } 216 | .modal { 217 | 218 | width: 600px; 219 | height: 400px; 220 | /* line-height: 200px;*/ 221 | position: fixed; 222 | top: 50%; 223 | left: 50%; 224 | margin-top: -200px; 225 | margin-left: -300px; 226 | background-color: #bfbfbf; 227 | border-radius: 5px; 228 | text-align: center; 229 | z-index: 11; /* 1px higher than the overlay layer */ 230 | } 231 | 232 | .statusDiv{ 233 | margin-right: 10px; 234 | margin-left: 10px; 235 | margin-top: 10px; 236 | margin-bottom: 5px; 237 | padding: 0px; 238 | height: 82%; 239 | width: 100%; 240 | overflow: auto; 241 | text-align: left; 242 | font-size:85%; 243 | font-family: serif; 244 | border-style: solid; 245 | border-width: 2px; 246 | } 247 | .vidiv{ 248 | 249 | overflow: auto; 250 | font-size:85%; 251 | font-family: serif; 252 | border-style: solid; 253 | border-width: 2px; 254 | } 255 | -------------------------------------------------------------------------------- /mhserver/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Myhome Config 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

My home monitoring system

19 |
20 | 21 | 22 | 25 | 26 | 27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 37 |

38 | 39 |

40 | 41 |

42 | 43 |

44 | 45 |

46 |
47 |
48 |
49 |
50 | 51 | 52 |
53 | Status 54 |
55 |
56 | 57 |
58 | 59 |
60 | 61 | 62 | 63 |

64 |
65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /mhserver/view/peer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Browser base64 Session Description
5 | Golang base64 Session Description:
6 | 7 | 8 |
9 | 58 | -------------------------------------------------------------------------------- /mhserver/view/scripts/control.js: -------------------------------------------------------------------------------- 1 | var editor=null; 2 | var ws; 3 | var configlist; 4 | var container; 5 | var editor; 6 | var statusdiv; 7 | var editdiv; 8 | var list; 9 | var currentConfig=null; 10 | var msgtext; 11 | var peers=[{}]; 12 | var localid; 13 | var alllights = {Type:"light", Name: "light", Desc: "all lights", State: "online", Address:"all"}; 14 | var controlmsgs; 15 | 16 | //var shutdowndlg; 17 | //var shutdowndlgbutton; 18 | 19 | 20 | 21 | if ("WebSocket" in window) { 22 | 23 | 24 | function init(){ 25 | 26 | 27 | var MESSAGE_SERVER = (location.protocol == 'https:' ? 'wss' : 'ws') + '://'+ document.domain +':8080/ws'; 28 | ws = new WebSocket(MESSAGE_SERVER); 29 | // ws = new WebSocket("ws://172.20.3.50:8080/ws"); 30 | 31 | 32 | ws.onopen = function(event) { 33 | //msgarea.nodeValue="connected to server "; 34 | 35 | var msg = {Type:"status", Msg:{ Name: "unknown", Type: "text", Desc: location.hostname, State: "online"}}; 36 | ws.send(JSON.stringify(msg)); 37 | var item = document.createElement("div"); 38 | item.innerHTML = "Status sent"; 39 | appendLog(item); 40 | addctrllist("camera"); 41 | addctrllist("hmi"); 42 | 43 | 44 | 45 | }; 46 | 47 | ws.onerror = function(event) { 48 | 49 | var item = document.createElement("div"); 50 | item.innerHTML = "Websocket error"; 51 | appendLog(item); 52 | 53 | 54 | 55 | }; 56 | 57 | ws.onmessage = function(event) { 58 | 59 | 60 | var packet = JSON.parse(event.data); 61 | 62 | if(packet.Type == "control"){ 63 | if(packet.Msg === null) 64 | return; 65 | 66 | /* 67 | 68 | if(packet.Msg.Type == "notification"){ 69 | status = packet.Msg.Value; 70 | 71 | var div = document.createElement("Div"); 72 | div.innerHTML = status; 73 | statusdiv.appendChild(div); 74 | statusDiv.scrollTop = statusDiv.scrollHeight; 75 | } 76 | */ 77 | if(packet.Msg.Type == "id"){ 78 | localid = packet.Msg.Value; 79 | var msg1 = {Msg: {Name: "listdevices", Value: {Type: "all"}}, Targets: ["hmi"], Type: "command"}; 80 | ws.send(JSON.stringify(msg1)); 81 | } 82 | 83 | 84 | if(packet.Msg.Type == "devicelist" ){ 85 | 86 | 87 | packet.Msg.Value.push({Type:"light", Name: "light", Desc: "all lights", State: "online", Address:"all"}); 88 | packet.Msg.Value.push({Type:"camera", Name: "camera", Desc: "all cameras", State: "online", Address:"all"}); 89 | packet.Msg.Value.push({Type:"hmi", Name: "hmi", Desc: "all HMIs", State: "online", Address:"all"}); 90 | 91 | 92 | 93 | 94 | for(cnt=0; cnt < packet.Msg.Value.length; ++cnt){ 95 | 96 | var dev = packet.Msg.Value[cnt]; 97 | var devtype = dev.Type; 98 | if(devtype === "light" || devtype === "camera" || devtype==="hmi"){ 99 | adddev(dev); 100 | } else { 101 | adddevmisc 102 | } 103 | 104 | } 105 | } 106 | 107 | if(packet.Msg.Type == "msglist" ){ 108 | controlmsgs = packet.Msg.Value; 109 | camctrllist = document.getElementById("camerasel"); 110 | hmictrllist = document.getElementById("hmisel"); 111 | while (camctrllist.firstChild) { 112 | camctrllist.removeChild(camctrllist.firstChild); 113 | 114 | } 115 | while (hmictrllist.firstChild) { 116 | hmictrllist.removeChild(hmictrllist.firstChild); 117 | 118 | } 119 | 120 | 121 | Object.keys(packet.Msg.Value).forEach(function(key){ 122 | if(packet.Msg.Value[key].Targets != null){ 123 | 124 | if(packet.Msg.Value[key].Targets[0]==="camera"){ 125 | var option = document.createElement("option"); 126 | option.innerHTML=key.substring(0, key.indexOf(".json")); 127 | option.value=key; 128 | camctrllist.appendChild(option); 129 | 130 | } 131 | if(packet.Msg.Value[key].Targets[0]==="hmi"){ 132 | var option = document.createElement("option"); 133 | option.innerHTML=key.substring(0, key.indexOf(".json")); 134 | option.value=key; 135 | hmictrllist.appendChild(option); 136 | } 137 | } 138 | }); 139 | 140 | 141 | } 142 | 143 | if (packet.Msg.Type == "configuration"){ 144 | //editor.set(packet.Msg.Value); 145 | // send the control 146 | } 147 | 148 | } 149 | 150 | 151 | }; // end of onmessage 152 | 153 | ws.onclose = function(event) { 154 | var item = document.createElement("div"); 155 | item.innerHTML = "Websocket has been closed"; 156 | appendLog(item); 157 | }; 158 | 159 | }; // end of init 160 | 161 | function error(err) { console.log("crap")} 162 | 163 | function appendLog(item) { 164 | var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1; 165 | log.appendChild(item); 166 | if (doScroll) { 167 | log.scrollTop = log.scrollHeight - log.clientHeight; 168 | } 169 | } 170 | 171 | 172 | 173 | function sendSwitchControl(ev){ 174 | devicetype = ev.target.dataset.devtype; 175 | 176 | var checked="off"; 177 | if(ev.target.checked) 178 | checked = "on"; 179 | // gang the switches 180 | if(ev.target.id == devicetype){ 181 | devs = document.getElementById(devicetype+"s").querySelectorAll("input") 182 | Object.keys(devs).forEach(function(key){ 183 | devs[key].checked = ev.target.checked; 184 | }); 185 | 186 | 187 | } 188 | 189 | switch(ev.target.dataset.devtype){ 190 | 191 | case "light": 192 | 193 | ws.send(JSON.stringify({Msg:{Name:"mode",Value:checked},Targets:[ev.target.name],Type:"control"})); 194 | 195 | break; 196 | 197 | 198 | case "camera": 199 | /* 200 | sel = document.getElementById("camerasel"); 201 | msgname=sel.options[sel.selectedIndex].text; 202 | msg = controlmsgs[msgname+".json"]; 203 | msg.Targets=[ev.target.name]; 204 | ws.send(JSON.stringify(msg)); 205 | 206 | devs = document.getElementById(devicetype+"s").querySelectorAll("input") 207 | Object.keys(devs).forEach(function(key){ 208 | devs[key].checked = 0; 209 | }); 210 | */ 211 | break; 212 | 213 | case "hmi": 214 | // ws.send(JSON.stringify({Msg:{Name:"mode",Value:checked},Targets:[ev.target.name],Type:"control"})); 215 | 216 | break; 217 | 218 | case "misc": 219 | // ws.send(JSON.stringify({Msg:{Name:"mode",Value:checked},Targets:[ev.target.name],Type:"control"})); 220 | 221 | break; 222 | 223 | 224 | } 225 | } 226 | 227 | 228 | 229 | function sendSubmitControl(ev){ 230 | 231 | 232 | 233 | switch(ev.target.dataset.devtype){ 234 | 235 | 236 | case "camera": 237 | sel = document.getElementById("camerasel"); 238 | msgname=sel.options[sel.selectedIndex].text; 239 | msg = controlmsgs[msgname+".json"]; 240 | devices = []; 241 | devs = document.getElementById(devicetype+"s").querySelectorAll("input") 242 | Object.keys(devs).forEach(function(key){ 243 | devname = devs[key].name 244 | if(devs[key].checked){ 245 | devices.push(devname); 246 | if(devname === "camera") 247 | devices=["camera"]; 248 | } 249 | 250 | devs[key].checked = 0; 251 | 252 | 253 | }); 254 | msg.Targets=devices; 255 | ws.send(JSON.stringify(msg)); 256 | 257 | break; 258 | 259 | case "hmi": 260 | sel = document.getElementById("hmisel"); 261 | msgname=sel.options[sel.selectedIndex].text; 262 | msg = controlmsgs[msgname+".json"]; 263 | devices = []; 264 | devs = document.getElementById(devicetype+"s").querySelectorAll("input") 265 | Object.keys(devs).forEach(function(key){ 266 | devname = devs[key].name 267 | if(devs[key].checked){ 268 | devices.push(devname); 269 | if(devname === "hmi") 270 | devices=["hmi"]; 271 | } 272 | 273 | devs[key].checked = 0; 274 | 275 | 276 | }); 277 | msg.Targets=devices; 278 | ws.send(JSON.stringify(msg)); 279 | 280 | break; 281 | 282 | case "misc": 283 | // ws.send(JSON.stringify({Msg:{Name:"mode",Value:checked},Targets:[ev.target.name],Type:"control"})); 284 | 285 | break; 286 | 287 | 288 | } 289 | 290 | } 291 | 292 | 293 | function addctrllist(devtype){ 294 | var dvdiv = document.getElementById(devtype+"s"); 295 | var div = document.createElement("div"); 296 | div.className = "field"; 297 | var inp = document.createElement("select"); 298 | inp.id = devtype+"sel"; 299 | inp.dataset.devtype=devtype; 300 | 301 | 302 | var el = document.createElement("button"); 303 | el.dataset.devtype=devtype; 304 | el.addEventListener("click", sendSubmitControl); 305 | el.innerHTML="Submit"; 306 | 307 | 308 | div.appendChild(inp); 309 | div.appendChild(el); 310 | dvdiv.appendChild(div); 311 | } 312 | 313 | 314 | 315 | function adddev(dev){ 316 | var devdiv = document.getElementById(dev.Type+"s"); 317 | if (devdiv === null) 318 | devdiv = document.getElementById("miscs"); 319 | var div = document.createElement("div"); 320 | div.className = "field"; 321 | var inp = document.createElement("input"); 322 | inp.type = "checkbox"; 323 | inp.id = dev.Name; 324 | inp.name = dev.Name; 325 | inp.innerHTML = dev.Desc; 326 | inp.className = "switch"; 327 | inp.checked = ""; 328 | inp.addEventListener("click", sendSwitchControl); 329 | inp.dataset.devtype=dev.Type; 330 | var lbl = document.createElement("label"); 331 | lbl.htmlFor = dev.Name; 332 | lbl.innerHTML=dev.Desc; 333 | div.appendChild(inp); 334 | div.appendChild(lbl); 335 | devdiv.appendChild(div); 336 | } 337 | 338 | 339 | 340 | function adddevmisc(dev){} 341 | 342 | function sendmsg(){ 343 | var name = configlist.options[configlist.selectedIndex].text; 344 | 345 | var msg = editor.get(); 346 | 347 | if(name == "note.json") 348 | msg.Msg.Value = msgtext.value; 349 | 350 | 351 | ws.send(JSON.stringify(msg)); 352 | 353 | } 354 | 355 | function loadConfig(){ 356 | var name = editconfiglist.options[editconfiglist.selectedIndex].text; 357 | 358 | var msg = {Type: "command", Msg:{Name: "loadmsg", Value: {filename: name}}}; 359 | ws.send(JSON.stringify(msg)); 360 | } 361 | 362 | 363 | 364 | 365 | } 366 | -------------------------------------------------------------------------------- /mhserver/view/scripts/img/jsoneditor-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machinesworking/myhome/f587956724c2c14a66511f97694a69508b8b5651/mhserver/view/scripts/img/jsoneditor-icons.png -------------------------------------------------------------------------------- /mhserver/view/scripts/peer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function MyPeer(peername, ws){ 3 | 4 | this.localname = peername; 5 | 6 | 7 | let pc = new RTCPeerConnection({ 8 | iceServers: [ 9 | { 10 | urls: 'stun:stun.l.google.com:19302' 11 | } 12 | ] 13 | }) 14 | let log = msg => { 15 | // document.getElementById('vstat').innerHTML += peername+": "+msg + '
' 16 | console.log(msg); 17 | } 18 | 19 | pc.ontrack = function (event) { 20 | var el = document.createElement(event.track.kind) 21 | el.srcObject = event.streams[0] 22 | el.autoplay = true 23 | el.controls = true 24 | el.width = "640" 25 | el.height = "480" 26 | document.getElementById(peername).appendChild(el) 27 | } 28 | 29 | pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) 30 | pc.onicecandidate = event => { 31 | if(event.candidate === null){ 32 | var msg = new Object(); 33 | msg.Type = "control"; 34 | msg.Targets = [peername]; 35 | msg.Msg = {Type: "offer", Value: {from: localid, data: pc.localDescription.sdp}}; 36 | ws.send(JSON.stringify(msg)); 37 | } 38 | } 39 | console.log("creating offer") 40 | pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).then(d => pc.setLocalDescription(d)).catch(log) 41 | return pc; 42 | /* 43 | pc.startsess = function(d){ 44 | try { 45 | pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: d})) 46 | } catch (e) { 47 | alert(e) 48 | } 49 | } 50 | */ 51 | } 52 | -------------------------------------------------------------------------------- /mhserver/view/scripts/seq.js: -------------------------------------------------------------------------------- 1 | var editor=null; 2 | var ws; 3 | var configlist; 4 | var container; 5 | var editor; 6 | var statusdiv; 7 | var editdiv; 8 | var list; 9 | var currentConfig=null; 10 | var msgtext; 11 | var peers=[{}]; 12 | var localid; 13 | 14 | //var shutdowndlg; 15 | //var shutdowndlgbutton; 16 | var nightmodestate = "off"; 17 | var options = { 18 | mode: 'tree', 19 | modes: ['code', 'form', 'text', 'tree', 'view'], // allowed modes 20 | error: function (err) { 21 | alert(err.toString()); 22 | } 23 | }; 24 | 25 | 26 | 27 | if ("WebSocket" in window) { 28 | 29 | 30 | function init(){ 31 | editconfiglist = document.getElementById("editConfigSelect"); 32 | configlist = document.getElementById("configSelect"); 33 | container = document.getElementById("jsoneditor"); 34 | msgtext = document.getElementById("msgtext"); 35 | 36 | 37 | statusdiv = document.getElementById("statusDiv"); 38 | 39 | 40 | status = document.getElementById("status"); 41 | 42 | editdiv = document.getElementById("editDiv"); 43 | 44 | editor = new JSONEditor(container, options); 45 | 46 | 47 | configlist.addEventListener("change", function() { 48 | var name = configlist.options[configlist.selectedIndex].text; 49 | currentConfig = name; 50 | 51 | 52 | if(name == "note.json") 53 | msgtext.style.visibility="visible"; 54 | else 55 | msgtext.style.visibility="hidden"; 56 | 57 | var msg = {Type: "command", Msg:{Name: "loadmsg", Value: {filename: name}}}; 58 | 59 | 60 | ws.send(JSON.stringify(msg)); 61 | 62 | }); 63 | 64 | editconfiglist.addEventListener("change", function() { 65 | loadConfig(); 66 | }); 67 | 68 | 69 | var MESSAGE_SERVER = (location.protocol == 'https:' ? 'wss' : 'ws') + '://'+ document.domain +':8080/ws'; 70 | ws = new WebSocket(MESSAGE_SERVER); 71 | // ws = new WebSocket("ws://172.20.3.50:8080/ws"); 72 | 73 | 74 | ws.onopen = function(event) { 75 | //msgarea.nodeValue="connected to server "; 76 | 77 | var msg = {Type:"status", Msg:{ Name: "unknown", Type: "text", Desc: location.hostname, State: "online"}}; 78 | ws.send(JSON.stringify(msg)); 79 | 80 | 81 | }; 82 | 83 | ws.onerror = function(event) { 84 | }; 85 | 86 | ws.onmessage = function(event) { 87 | 88 | 89 | var packet = JSON.parse(event.data); 90 | 91 | if(packet.Type == "control"){ 92 | if(packet.Msg.Type == "notification"){ 93 | status = packet.Msg.Value; 94 | 95 | var div = document.createElement("Div"); 96 | div.innerHTML = status; 97 | statusdiv.appendChild(div); 98 | statusDiv.scrollTop = statusDiv.scrollHeight; 99 | } 100 | 101 | if(packet.Msg.Type == "id"){ 102 | localid = packet.Msg.Value; 103 | } 104 | 105 | 106 | if(packet.Msg.Type == "devicelist" ){ 107 | 108 | 109 | for(cnt=0; cnt < packet.Msg.Value.length; ++cnt){ 110 | var div = document.createElement("Div"); 111 | div.innerHTML = packet.Msg.Value[cnt].Desc +" "+packet.Msg.Value[cnt].State; 112 | statusdiv.appendChild(div); 113 | statusDiv.scrollTop = statusDiv.scrollHeight; 114 | } 115 | 116 | 117 | } 118 | /* 119 | if(packet.Msg.Type == "peerlist" ){ 120 | var peercnt = packet.Msg.Value.length; 121 | for(cnt=0; cnt < peercnt; ++cnt){ 122 | var peername = packet.Msg.Value[cnt]; 123 | console.log("creating connection for "+peername); 124 | var peer = new MyPeer( peername, ws ); 125 | peers[cnt]={sessionid: peername, peer: peer}; 126 | } 127 | } 128 | 129 | if(packet.Msg.Type == "answer"){ 130 | 131 | for(var z=0; z=7)) 215 | 216 | } 217 | 218 | var data = editor.get(); 219 | 220 | var msg = {Type: "command", Msg:{Name: "savemsg", Value:{Name: name, Value: data}}}; 221 | ws.send(JSON.stringify(msg)); 222 | } 223 | 224 | function loadConfig(){ 225 | var name = editconfiglist.options[editconfiglist.selectedIndex].text; 226 | 227 | var msg = {Type: "command", Msg:{Name: "loadmsg", Value: {filename: name}}}; 228 | ws.send(JSON.stringify(msg)); 229 | } 230 | 231 | function doneEditing(){ 232 | var editdiv = document.getElementById("editDiv"); 233 | editDiv.style.display="none"; 234 | } 235 | 236 | function deleteConfig(){ 237 | var name = editconfiglist.options[editconfiglist.selectedIndex].text; 238 | var msg = {Type: "command", Msg:{Name: "deletemsg", Value:{filename: name}}}; 239 | ws.send(JSON.stringify(msg)); 240 | } 241 | 242 | 243 | function editConfig(){ 244 | 245 | editDiv.style.display="block"; 246 | } 247 | 248 | 249 | 250 | 251 | function sendmsg(){ 252 | var name = configlist.options[configlist.selectedIndex].text; 253 | 254 | var msg = editor.get(); 255 | 256 | if(name == "note.json") 257 | msg.Msg.Value = msgtext.value; 258 | 259 | 260 | ws.send(JSON.stringify(msg)); 261 | 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /mhserver/view/scripts/single.js: -------------------------------------------------------------------------------- 1 | var ws; 2 | var peers=[{}]; 3 | var localid; 4 | 5 | 6 | 7 | 8 | if ("WebSocket" in window) { 9 | 10 | 11 | function init(){ 12 | 13 | var MESSAGE_SERVER = (location.protocol == 'https:' ? 'wss' : 'ws') + '://'+ document.domain +':8080/ws'; 14 | ws = new WebSocket(MESSAGE_SERVER); 15 | // ws = new WebSocket("ws://172.20.3.50:8080/ws"); 16 | 17 | 18 | ws.onopen = function(event) { 19 | //msgarea.nodeValue="connected to server "; 20 | 21 | var msg = {Type:"status", Msg:{ Name: "unknown", Type: "text", Desc: location.hostname, State: "online"}}; 22 | ws.send(JSON.stringify(msg)); 23 | 24 | 25 | }; 26 | 27 | ws.onerror = function(event) { 28 | }; 29 | 30 | ws.onmessage = function(event) { 31 | 32 | 33 | var packet = JSON.parse(event.data); 34 | 35 | if(packet.Type == "control"){ 36 | 37 | 38 | if(packet.Msg.Type == "id"){ 39 | localid = packet.Msg.Value; 40 | } 41 | 42 | 43 | if(packet.Msg.Type == "peerlist" ){ 44 | var peercnt = packet.Msg.Value.length; 45 | for(cnt=0; cnt < peercnt; ++cnt){ 46 | var peername = packet.Msg.Value[cnt]; 47 | console.log("creating connection for "+peername); 48 | 49 | var peer = new MyPeer( peername, ws ); 50 | peers[cnt]={sessionid: peername, peer: peer}; 51 | } 52 | var tableelt = document.getElementById("videotable"); 53 | var pcnt=0; 54 | for (var irow=0; irow<2; irow++) { 55 | var row = tableelt.insertRow(0); 56 | for (var icol=0; icol<2; icol++) { 57 | var vidiv = document.createElement("div"); 58 | vidiv.id = peers[pcnt].sessionid; 59 | vidiv.innerHTML = vidiv.id+'
'; 60 | vidiv.className = 'vidiv'; 61 | ++pcnt; 62 | row.insertCell(0).appendChild(vidiv); 63 | } 64 | } 65 | 66 | 67 | } 68 | if(packet.Msg.Type == "answer"){ 69 | 70 | for(var z=0; z 0 { 67 | writeNALU(false, int(ts), payload) 68 | } 69 | } 70 | } 71 | if err := Client.Open(url); err != nil { 72 | fmt.Println("[RTSP] Error", err) 73 | } else { 74 | for { 75 | select { 76 | case <-Client.Signals: 77 | fmt.Println("Exit signals by rtsp") 78 | sps = []byte{} 79 | pps = []byte{} 80 | fuBuffer = []byte{} 81 | count = 0 82 | Client = rtsp.RtspClientNew() 83 | Client.Debug = false 84 | syncCount = 0 85 | preTS = 0 86 | RETRY1: 87 | if err := Client.Open(url); err != nil { 88 | fmt.Println("[RTSP] Error", err) 89 | time.Sleep(time.Second * 10) 90 | goto RETRY1 91 | } 92 | 93 | case <-cancelchan: 94 | Client.Close() 95 | case data := <-Client.Outgoing: 96 | count += len(data) 97 | //fmt.Println("recive rtp packet size", len(data), "recive all packet size", count) 98 | if data[0] == 36 && data[1] == 0 { 99 | cc := data[4] & 0xF 100 | rtphdr := 12 + cc*4 101 | ts := (int64(data[8]) << 24) + (int64(data[9]) << 16) + (int64(data[10]) << 8) + (int64(data[11])) 102 | packno := (int64(data[6]) << 8) + int64(data[7]) 103 | if false { 104 | log.Println("packet num", packno) 105 | } 106 | nalType := data[4+rtphdr] & 0x1F 107 | if nalType >= 1 && nalType <= 23 { 108 | handleNALU(nalType, data[4+rtphdr:], ts) 109 | } else if nalType == 28 { 110 | isStart := data[4+rtphdr+1]&0x80 != 0 111 | isEnd := data[4+rtphdr+1]&0x40 != 0 112 | nalType := data[4+rtphdr+1] & 0x1F 113 | nal := data[4+rtphdr]&0xE0 | data[4+rtphdr+1]&0x1F 114 | if isStart { 115 | fuBuffer = []byte{0} 116 | } 117 | fuBuffer = append(fuBuffer, data[4+rtphdr+2:]...) 118 | if isEnd { 119 | fuBuffer[0] = nal 120 | handleNALU(nalType, fuBuffer, ts) 121 | } 122 | } 123 | } else if data[0] == 36 && data[1] == 2 { 124 | //cc := data[4] & 0xF 125 | //rtphdr := 12 + cc*4 126 | //payload := data[4+rtphdr+4:] 127 | } 128 | } 129 | } 130 | } 131 | Client.Close() 132 | } 133 | -------------------------------------------------------------------------------- /mycam/static/demo.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mycam/static/index.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Browser base64 Session Description
4 | Golang base64 Session Description:
5 | 6 | 7 |
8 | 63 | -------------------------------------------------------------------------------- /mycam/webrtc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machinesworking/myhome/f587956724c2c14a66511f97694a69508b8b5651/mycam/webrtc.png -------------------------------------------------------------------------------- /pivoice/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 IBM Corp. 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * which accompanies this distribution, and is available at 7 | * http://www.eclipse.org/legal/epl-v10.html 8 | * 9 | * Contributors: 10 | * Seth Hoenig 11 | * Allan Stockdill-Mander 12 | * Mike Robertson 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "encoding/json" 19 | "flag" 20 | "fmt" 21 | "log" 22 | "net/url" 23 | "os/exec" 24 | "time" 25 | 26 | "github.com/gorilla/websocket" 27 | ) 28 | 29 | type device struct { 30 | Type string 31 | Name string 32 | Desc string 33 | State string 34 | } 35 | 36 | type rawmsg struct { 37 | Type string 38 | Value interface{} 39 | } 40 | 41 | type message struct { 42 | Type string 43 | Msg json.RawMessage 44 | } 45 | 46 | func check(e error) { 47 | if e != nil { 48 | fmt.Println(e.Error()) 49 | } 50 | } 51 | 52 | func main() { 53 | angelos := flag.String("name", "angelos10", " -name The name of this Angelos") 54 | devicedesc := flag.String("desc", "angelos Ten", " -desc The description of this Angelos") 55 | 56 | wsServer := flag.String("server", "192.168.50.13:8080", "-server The webserver to contact") 57 | audioPath := flag.String("apath", "/home/pi/audio", "-apath pathtoaudio") 58 | 59 | flag.Parse() 60 | speech := Speech{Folder: *audioPath, Language: "en"} 61 | // origin := "http://" + *angelos + ".local/" 62 | 63 | // sig := make(chan os.Signal, 1) 64 | // signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 65 | Start: 66 | 67 | log.SetFlags(0) 68 | 69 | // interrupt := make(chan os.Signal, 1) 70 | // signal.Notify(interrupt, os.Interrupt) 71 | 72 | u := url.URL{Scheme: "ws", Host: *wsServer, Path: "/ws"} 73 | log.Printf("connecting to %s", u.String()) 74 | 75 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 76 | if err != nil { 77 | 78 | speech.Speak("I am sorry but I cannot connect to the server.") 79 | 80 | // log. Fatal(err) 81 | log.Printf("dial: %s", err.Error()) 82 | 83 | time.Sleep(10 * time.Second) 84 | goto Start 85 | 86 | } 87 | defer c.Close() 88 | 89 | //done := make(chan struct{}) 90 | 91 | dv := device{Type: "hmi", Name: *angelos, Desc: *devicedesc, State: "online"} 92 | dev, _ := json.Marshal(dv) 93 | status := message{Type: "status", Msg: dev} 94 | err = c.WriteJSON(&status) 95 | 96 | // err = websocket.JSON.Send(ws, &status) 97 | if err != nil { 98 | 99 | speech.Speak("I am sorry but I cannot send to the server.") 100 | 101 | time.Sleep(10 * time.Second) 102 | 103 | goto Start 104 | 105 | } 106 | 107 | msg := new(message) 108 | for { 109 | log.Println("waiting for message from server") 110 | err = c.ReadJSON(&msg) 111 | // fmt.Printf("%v", msg) 112 | // err = websocket.JSON.Receive(ws, &rcvbuf) 113 | if err != nil { 114 | 115 | speech.Speak("I am sorry but I cannot receive from the server.") 116 | 117 | time.Sleep(10 * time.Second) 118 | 119 | goto Start 120 | } 121 | switch msg.Type { 122 | case "control": 123 | var ctrl = new(rawmsg) 124 | 125 | err := json.Unmarshal(msg.Msg, &ctrl) 126 | if err != nil { 127 | fmt.Printf("failed to decode control: %s\n", err.Error()) 128 | 129 | } 130 | 131 | switch ctrl.Type { 132 | 133 | case "sound": 134 | if ctrl.Value.(string) == "off" { 135 | speech.Speak("Sound will be set to " + ctrl.Value.(string)) 136 | } 137 | cmd := exec.Command("amixer", "set", "'Master'", ctrl.Value.(string)) 138 | cmd.Run() 139 | if ctrl.Value == "on" { 140 | speech.Speak("Sound will be set to " + ctrl.Value.(string)) 141 | } 142 | 143 | case "volume": 144 | cmd := exec.Command("amixer", "set", "'Master'", ctrl.Value.(string)) 145 | cmd.Run() 146 | speech.Speak("Volume has been set to " + ctrl.Value.(string)) 147 | 148 | case "notification": 149 | 150 | err = speech.Speak(ctrl.Value.(string)) 151 | 152 | if err != nil { 153 | fmt.Printf("speak returned error: %s", err.Error()) 154 | continue 155 | } 156 | 157 | } 158 | } 159 | 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /pivoice/picgotts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | /** 13 | * Required: 14 | * - mplayer 15 | * 16 | * Use: 17 | * 18 | * speech := htgotts.Speech{Folder: "audio", Language: "en"} 19 | */ 20 | 21 | // Speech struct 22 | type Speech struct { 23 | Folder string 24 | Language string 25 | } 26 | 27 | // Speak downloads speech and plays it using aplay 28 | func (speech *Speech) Speak(text string) error { 29 | 30 | fileName := speech.Folder + "/" + text + ".wav" 31 | 32 | var err error 33 | if err = speech.createFolderIfNotExists(speech.Folder); err != nil { 34 | return err 35 | } 36 | if err = speech.createIfNotExists(fileName, text); err != nil { 37 | return err 38 | } 39 | 40 | return speech.play(fileName) 41 | } 42 | 43 | /** 44 | * Create the folder if does not exists. 45 | */ 46 | func (speech *Speech) createFolderIfNotExists(folder string) error { 47 | dir, err := os.Open(folder) 48 | if os.IsNotExist(err) { 49 | fmt.Printf("creating directory\n") 50 | 51 | return os.MkdirAll(folder, 0700) 52 | } 53 | 54 | dir.Close() 55 | // fmt.Printf("directory exists\n") 56 | 57 | return nil 58 | } 59 | 60 | func (speech *Speech) createIfNotExists(fileName string, text string) error { 61 | 62 | if _, err := os.Stat(fileName); os.IsNotExist(err) { 63 | // fmt.Printf("Creating new voice file: %s", fileName) 64 | pico2wave := exec.Command("pico2wave", "-w", fileName, text) 65 | pico2wave.Run() 66 | } 67 | 68 | // fmt.Printf("Voice file exists: %s", fileName) 69 | 70 | return nil 71 | } 72 | 73 | /** 74 | * Download the voice file if does not exists. 75 | */ 76 | func (speech *Speech) downloadIfNotExists(fileName string, text string) error { 77 | f, err := os.Open(fileName) 78 | if err != nil { 79 | url := fmt.Sprintf("http://translate.google.com/translate_tts?ie=UTF-8&total=1&idx=0&textlen=32&client=tw-ob&q=%s&tl=%s", url.QueryEscape(text), speech.Language) 80 | response, err := http.Get(url) 81 | if err != nil { 82 | return err 83 | } 84 | defer response.Body.Close() 85 | 86 | output, err := os.Create(fileName) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | _, err = io.Copy(output, response.Body) 92 | return err 93 | } 94 | 95 | f.Close() 96 | return nil 97 | } 98 | 99 | /** 100 | * Play voice file. 101 | */ 102 | func (speech *Speech) play(fileName string) error { 103 | // fmt.Printf("Playing %s\n", fileName) 104 | aplay := exec.Command("/usr/bin/aplay", fileName) 105 | return aplay.Run() 106 | } 107 | --------------------------------------------------------------------------------