├── .gitignore ├── huestate.go ├── response.go ├── request.go ├── examples └── huejack.go ├── README.md ├── handler.go ├── upnp.go └── lights.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/** 3 | -------------------------------------------------------------------------------- /huestate.go: -------------------------------------------------------------------------------- 1 | package huejack 2 | 3 | type huestate struct { 4 | Handler Handler 5 | OnState bool 6 | } -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package huejack 2 | 3 | 4 | type Response struct { 5 | OnState bool 6 | ErrorState bool 7 | } -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package huejack 2 | 3 | 4 | type Request struct { 5 | UserId string 6 | RequestedOnState bool 7 | RemoteAddr string 8 | } -------------------------------------------------------------------------------- /examples/huejack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pborges/huejack" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | huejack.SetLogger(os.Stdout) 11 | huejack.Handle("test", func(req huejack.Request, res *huejack.Response) { 12 | fmt.Println("im handling test from", req.RemoteAddr, req.RequestedOnState) 13 | res.OnState = req.RequestedOnState 14 | // res.ErrorState = true //set ErrorState to true to have the echo respond with "unable to reach device" 15 | return 16 | }) 17 | 18 | // it is very important to use a full IP here or the UPNP does not work correctly. 19 | // one day ill fix this 20 | panic(huejack.ListenAndServe("192.168.2.103:5000")) 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # huejack 2 | ## A bare bones emulator library for the Phillips Hue bulb system to be used with the Amazon Echo (Alexa) 3 | 4 | ### Acknowledgements 5 | Thanks to armzilla for doing all the real work ([https://github.com/armzilla/amazon-echo-ha-bridge](https://github.com/armzilla/amazon-echo-ha-bridge)) 6 | 7 | I feel this implementation is a little heavy. 8 | 9 | I wanted a library rather then a server for easy extensibility. 10 | 11 | I wanted to remove alot of code that I felt was _voodoo_ magic. 12 | 13 | The only difference between this library and [this one](https://github.com/pborges/huemulator) is that in this library I attempted to emulate the golang http package and remove the need for a database of lights 14 | ### Example 15 | see ```examples/huejack.go``` 16 | 17 | ### Tips 18 | 19 | Returning true for error in a huejack.Handler will make the echo reply with "Sorry the device is not responding" -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package huejack 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "net/http" 6 | "log" 7 | "io" 8 | "io/ioutil" 9 | ) 10 | 11 | var handlerMap map[string]huestate 12 | 13 | func init() { 14 | log.SetOutput(ioutil.Discard) 15 | handlerMap = make(map[string]huestate) 16 | upnpTemplateInit() 17 | } 18 | 19 | func SetLogger(w io.Writer) { 20 | log.SetOutput(w) 21 | } 22 | 23 | func ListenAndServe(addr string) error { 24 | router := httprouter.New() 25 | router.GET(upnp_uri, upnpSetup(addr)) 26 | 27 | router.GET("/api/:userId", getLightsList) 28 | router.PUT("/api/:userId/lights/:lightId/state", setLightState) 29 | router.GET("/api/:userId/lights/:lightId", getLightInfo) 30 | 31 | go upnpResponder(addr, upnp_uri) 32 | return http.ListenAndServe(addr, requestLogger(router)) 33 | } 34 | 35 | // Handler: 36 | // state is the state of the "light" after the handler function 37 | // if error is set to true echo will reply with "sorry the device is not responding" 38 | type Handler func(Request, *Response) 39 | 40 | func Handle(deviceName string, h Handler) { 41 | log.Println("[HANDLE]", deviceName) 42 | handlerMap[deviceName] = huestate{ 43 | Handler:h, 44 | OnState:false, 45 | } 46 | } 47 | 48 | func requestLogger(h http.Handler) http.Handler { 49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | log.Println("[WEB]", r.RemoteAddr, r.Method, r.URL) 51 | // log.Printf("\t%+v\n", r) 52 | h.ServeHTTP(w, r) 53 | }) 54 | } -------------------------------------------------------------------------------- /upnp.go: -------------------------------------------------------------------------------- 1 | package huejack 2 | import ( 3 | "github.com/julienschmidt/httprouter" 4 | "text/template" 5 | "net/http" 6 | "log" 7 | "net" 8 | "strings" 9 | "bytes" 10 | ) 11 | 12 | const ( 13 | upnp_multicast_address = "239.255.255.250:1900" 14 | upnp_uri = "/upnp/setup.xml" 15 | ) 16 | 17 | var responseTemplateText = 18 | `HTTP/1.1 200 OK 19 | CACHE-CONTROL: max-age=86400 20 | EXT: 21 | LOCATION: http://{{.}} 22 | OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 23 | ST: urn:schemas-upnp-org:device:basic:1 24 | USN: uuid:Socket-1_0-221438K0100073::urn:Belkin:device:** 25 | 26 | ` 27 | 28 | var setupTemplateText = 29 | ` 30 | 31 | 32 | 1 33 | 0 34 | 35 | http://{{.}}/ 36 | 37 | urn:schemas-upnp-org:device:Basic:1 38 | huejack 39 | Royal Philips Electronics 40 | Philips hue bridge 2012 41 | 929000226503 42 | uuid:f6543a06-800d-48ba-8d8f-bc2949eddc33 43 | 44 | ` 45 | 46 | type upnpData struct { 47 | Addr string 48 | Uri string 49 | } 50 | 51 | var setupTemplate *template.Template 52 | func upnpTemplateInit() { 53 | var err error 54 | setupTemplate, err = template.New("").Parse(setupTemplateText) 55 | if err != nil { 56 | log.Fatalln("upnpTemplateInit:", err) 57 | } 58 | } 59 | 60 | func upnpSetup(addr string) httprouter.Handle { 61 | return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 62 | w.Header().Set("Content-Type", "application/xml") 63 | err := setupTemplate.Execute(w, addr) 64 | if err != nil { 65 | log.Fatalln("[WEB] upnpSetup:", err) 66 | } 67 | } 68 | } 69 | 70 | 71 | func upnpResponder(hostAddr string, endpoint string) { 72 | responseTemplate, err := template.New("").Parse(responseTemplateText) 73 | 74 | log.Println("[UPNP] listening...") 75 | addr, err := net.ResolveUDPAddr("udp", upnp_multicast_address) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | l, err := net.ListenMulticastUDP("udp", nil, addr) 80 | l.SetReadBuffer(1024) 81 | 82 | for { 83 | b := make([]byte, 1024) 84 | n, src, err := l.ReadFromUDP(b) 85 | if err != nil { 86 | log.Fatal("[UPNP] ReadFromUDP failed:", err) 87 | } 88 | 89 | if strings.Contains(string(b[:n]), "MAN: \"ssdp:discover\"") { 90 | c, err := net.DialUDP("udp", nil, src) 91 | if err != nil { 92 | log.Fatal("[UPNP] DialUDP failed:", err) 93 | } 94 | 95 | log.Println("[UPNP] discovery request from", src) 96 | 97 | // For whatever reason I can't execute the template using c as the reader, 98 | // you HAVE to put it in a buffer first 99 | // possible timing issue? 100 | // don't believe me? try it 101 | b := &bytes.Buffer{} 102 | err = responseTemplate.Execute(b, hostAddr + endpoint) 103 | if err != nil { 104 | log.Fatal("[UPNP] execute template failed:", err) 105 | } 106 | c.Write(b.Bytes()) 107 | c.Close() 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lights.go: -------------------------------------------------------------------------------- 1 | package huejack 2 | import ( 3 | "github.com/julienschmidt/httprouter" 4 | "net/http" 5 | "encoding/json" 6 | "log" 7 | "strconv" 8 | ) 9 | 10 | type light struct { 11 | State struct { 12 | On bool `json:"on"` 13 | Bri int `json:"bri"` 14 | Hue int `json:"hue"` 15 | Sat int `json:"sat"` 16 | Effect string `json:"effect"` 17 | Ct int `json:"ct"` 18 | Alert string `json:"alert"` 19 | Colormode string `json:"colormode"` 20 | Reachable bool `json:"reachable"` 21 | XY []float64 `json:"xy"` 22 | } `json:"state"` 23 | Type string `json:"type"` 24 | Name string `json:"name"` 25 | ModelId string `json:"modelid"` 26 | ManufacturerName string `json:"manufacturername"` 27 | UniqueId string `json:"uniqueid"` 28 | SwVersion string `json:"swversion"` 29 | PointSymbol struct { 30 | One string `json:"1"` 31 | Two string `json:"2"` 32 | Three string `json:"3"` 33 | Four string `json:"4"` 34 | Five string `json:"5"` 35 | Six string `json:"6"` 36 | Seven string `json:"7"` 37 | Eight string `json:"8"` 38 | } `json:"pointsymbol"` 39 | } 40 | 41 | type lights struct { 42 | Lights map[string]light `json:"lights"` 43 | } 44 | 45 | func initLight(name string) light { 46 | l := light{ 47 | Type:"Extended color light", 48 | ModelId:"LCT001", 49 | SwVersion:"65003148", 50 | ManufacturerName:"Philips", 51 | Name:name, 52 | UniqueId:name, 53 | } 54 | l.State.Reachable = true 55 | l.State.XY = []float64{0.4255, 0.3998} // this seems to be voodoo, if it is nil the echo says it could not turn on/off the device, useful... 56 | return l 57 | } 58 | 59 | func enumerateLights() lights { 60 | lightList := lights{} 61 | lightList.Lights = make(map[string]light) 62 | for name, hstate := range handlerMap { 63 | l := initLight(name) 64 | l.State.On = hstate.OnState 65 | lightList.Lights[l.UniqueId] = l 66 | } 67 | return lightList 68 | } 69 | 70 | func getLightsList(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 71 | r.Body.Close() 72 | w.Header().Set("Content-Type", "application/json") 73 | err := json.NewEncoder(w).Encode(enumerateLights()) 74 | if err != nil { 75 | log.Fatalln("[WEB] Error encoding json", err) 76 | } 77 | } 78 | 79 | func setLightState(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 80 | defer r.Body.Close() 81 | w.Header().Set("Content-Type", "application/json") 82 | req := make(map[string]bool) 83 | json.NewDecoder(r.Body).Decode(&req) 84 | 85 | log.Println("[DEVICE]", p.ByName("userId"), "requested state:", req["on"]) 86 | response := Response{} 87 | if hstate, ok := handlerMap[p.ByName("lightId")]; ok { 88 | hstate.Handler(Request{ 89 | UserId:p.ByName("userId"), 90 | RequestedOnState:req["on"], 91 | RemoteAddr:r.RemoteAddr, 92 | }, &response) 93 | log.Println("[DEVICE] handler replied with state:", response.OnState) 94 | hstate.OnState = response.OnState 95 | handlerMap[p.ByName("lightId")] = hstate 96 | } 97 | if !response.ErrorState { 98 | w.Write([]byte("[{\"success\":{\"/lights/" + p.ByName("lightId") + "/state/on\":" + strconv.FormatBool(response.OnState) + "}}]")) 99 | } 100 | } 101 | 102 | func getLightInfo(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 103 | r.Body.Close() 104 | w.Header().Set("Content-Type", "application/json") 105 | 106 | l := initLight(p.ByName("lightId")) 107 | 108 | if hstate, ok := handlerMap[p.ByName("lightId")]; ok { 109 | if hstate.OnState { 110 | l.State.On = true 111 | } 112 | } 113 | 114 | err := json.NewEncoder(w).Encode(l) 115 | if err != nil { 116 | log.Fatalln("[WEB] Error encoding json", err) 117 | } 118 | } --------------------------------------------------------------------------------