github.com/aarzilli/tools@v0.0.0-20151123112009-0d27094f75e0/foscam/foscam.go (about) 1 // Package foscam allows setting and 2 // clearing alarm mode of such cam via http. 3 package foscam 4 5 import ( 6 "encoding/xml" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "math" 11 "net/http" 12 "strings" 13 "time" 14 15 _ "net/http/pprof" 16 17 "github.com/pbberlin/tools/appengine/util_appengine" 18 "github.com/pbberlin/tools/net/http/htmlfrag" 19 "github.com/pbberlin/tools/net/http/loghttp" 20 "github.com/pbberlin/tools/net/http/tplx" 21 "github.com/pbberlin/tools/stringspb" 22 "github.com/pbberlin/tools/util" 23 24 "appengine" 25 "appengine/urlfetch" 26 ) 27 28 var spf func(format string, a ...interface{}) string = fmt.Sprintf 29 var wpf func(w io.Writer, format string, a ...interface{}) (int, error) = fmt.Fprintf 30 31 var serveraddress string = `header of ["SERVER_ADDR"]` 32 var pos_01 int = strings.Index(serveraddress, "192.") 33 34 var dns_router string = "see func init" 35 var dns_cam string = "see func init" 36 37 const ( 38 debug = false 39 credentialsExcluded = "&usr=[a...n]&pwd=[pb...05]" 40 path_get_alarm = "/cgi-bin/CGIProxy.fcgi?cmd=getMotionDetectConfig" 41 path_set_alarm = "/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig" 42 43 path_snap_config = "/cgi-bin/CGIProxy.fcgi?cmd=setSnapConfig&snapPicQuality=0&saveLocation=2" 44 path_snap_retrieval = "/cgi-bin/CGIProxy.fcgi?cmd=snapPicture2" + credentials 45 path_video_retrieval = "/cgi-bin/CGIStream.cgi?cmd=GetMJStream" + credentials 46 47 path_get_log = "/cgi-bin/CGIProxy.fcgi?cmd=getLog&count=20&offset=0" 48 ) 49 50 // we have to upper case of fields to really get the values - to annoyed to think why 51 type CGI_Result struct { 52 Result string `xml:"result"` 53 IsEnable string `xml:"isEnable"` 54 Linkage string `xml:"linkage"` 55 SnapInterval string `xml:"snapInterval"` 56 Sensitivity string `xml:"sensitivity"` 57 TriggerInterval string `xml:"triggerInterval"` 58 Schedule0 string `xml:"schedule0"` 59 Schedule1 string `xml:"schedule1"` 60 Schedule2 string `xml:"schedule2"` 61 Schedule3 string `xml:"schedule3"` 62 Schedule4 string `xml:"schedule4"` 63 Schedule5 string `xml:"schedule5"` 64 Schedule6 string `xml:"schedule6"` 65 Area0 string `xml:"area0"` 66 Area1 string `xml:"area1"` 67 Area2 string `xml:"area2"` 68 Area3 string `xml:"area3"` 69 Area4 string `xml:"area4"` 70 Area5 string `xml:"area5"` 71 Area6 string `xml:"area6"` 72 Area7 string `xml:"area7"` 73 Area8 string `xml:"area8"` 74 Area9 string `xml:"area9"` 75 Log0 string `xml:"log0"` 76 Log1 string `xml:"log1"` 77 Log2 string `xml:"log2"` 78 Log3 string `xml:"log3"` 79 Log4 string `xml:"log4"` 80 Log5 string `xml:"log5"` 81 Log6 string `xml:"log6"` 82 Log7 string `xml:"log7"` 83 Log8 string `xml:"log8"` 84 Log9 string `xml:"log9"` 85 // XMLName xml.Name `xml:"account"` 86 } 87 88 func urlParamTS() string { 89 ts := time.Now().UnixNano() 90 return spf("%v", ts) 91 } 92 93 func makeRequest(w http.ResponseWriter, r *http.Request, path string) CGI_Result { 94 95 c := appengine.NewContext(r) 96 client := urlfetch.Client(c) 97 98 url_exe := spf(`http://%s%s%s&ts=%s`, dns_cam, path, credentials, urlParamTS()) 99 url_dis := spf(`http://%s%s&ts=%s`, dns_cam, path, urlParamTS()) 100 wpf(w, "<div style='font-size:10px; line-height:11px;'>requesting %v<br></div>\n", url_dis) 101 resp1, err := client.Get(url_exe) 102 loghttp.E(w, r, err, false) 103 104 bcont, err := ioutil.ReadAll(resp1.Body) 105 defer resp1.Body.Close() 106 loghttp.E(w, r, err, false) 107 108 cgiRes := CGI_Result{} 109 xmlerr := xml.Unmarshal(bcont, &cgiRes) 110 loghttp.E(w, r, xmlerr, false) 111 112 if cgiRes.Result != "0" { 113 wpf(w, "<b>RESPONSE shows bad mood:</b><br>\n") 114 psXml := stringspb.IndentedDump(cgiRes) 115 dis := strings.Trim(psXml, "{}") 116 wpf(w, "<pre style='font-size:10px;line-height:11px;'>%v</pre>", dis) 117 } 118 119 if debug { 120 scont := string(bcont) 121 wpf(w, "<pre style='font-size:10px;line-height:11px;'>%v</pre>", scont) 122 } 123 124 return cgiRes 125 126 } 127 128 func imageRetrieve(w http.ResponseWriter, r *http.Request) { 129 130 makeRequest(w, r, path_snap_config) 131 wpf(w, "<img src='http://%s%s' width='60%' /><br>", dns_cam, path_snap_retrieval) 132 133 } 134 135 func logRetrieve(w http.ResponseWriter, r *http.Request) { 136 137 cgiRes := makeRequest(w, r, path_get_log) 138 139 sl := []string{cgiRes.Log0, cgiRes.Log1, cgiRes.Log2, cgiRes.Log3, cgiRes.Log4, 140 cgiRes.Log5, cgiRes.Log6, cgiRes.Log7, cgiRes.Log8, cgiRes.Log9} 141 142 for _, v := range sl { 143 sl1 := strings.Split(v, "%2B") 144 if len(sl1) < 4 { 145 continue 146 } 147 148 // time+user+ip+logID 149 unixTS := sl1[0] 150 usr := sl1[1] 151 ip := sl1[2] 152 eventId := sl1[3] 153 eventDesc := "" 154 switch eventId { 155 case "0": 156 eventDesc = "Power On" 157 case "1": 158 eventDesc = "Motion Alarm" 159 case "3": 160 eventDesc = "Login" 161 case "4": 162 eventDesc = "Logout" 163 case "5": 164 eventDesc = "Offline" 165 default: 166 eventDesc = "unkown event id: " + eventId 167 } 168 _, _, _ = eventDesc, usr, ip 169 170 ts := util.TimeFromUnix(unixTS) 171 tsf := ts.Format("2.1.2006 15:04:05") 172 173 tn := time.Now() 174 since := tn.Sub(ts) 175 iHours := int(math.Floor(since.Hours())) 176 iMinutes := util.Round(since.Minutes()) - iHours*60 177 178 if eventId == "1" { 179 wpf(w, "Last Alarm <b>%3vhrs %2vmin</b> ago (%v)<br>\n", iHours, iMinutes, tsf) 180 break 181 } 182 } 183 184 } 185 186 func foscamStatus(w http.ResponseWriter, r *http.Request, m map[string]interface{}) { 187 188 htmlfrag.SetNocacheHeaders(w) 189 w.Header().Set("Content-Type", "text/html; charset=utf-8") 190 191 logRetrieve(w, r) 192 193 cgiRes := makeRequest(w, r, path_get_alarm) 194 195 psXml := stringspb.IndentedDump(cgiRes) 196 dis := strings.Trim(psXml, "{}") 197 dis = strings.Replace(dis, "\t", "", -1) 198 dis = strings.Replace(dis, " ", "", -1) 199 dis = strings.Replace(dis, "\"", "", -1) 200 dis = strings.Replace(dis, "\n", " ", -1) 201 dis = strings.Replace(dis, "Area0", "\nArea0", -1) 202 dis = strings.Replace(dis, "Schedule0", "\nSchedule0", -1) 203 dis = strings.Replace(dis, "Log0", "\nLog0", -1) 204 wpf(w, "<pre style='font-size:10px;line-height:11px;'>%v</pre>", dis) 205 206 if cgiRes.IsEnable == "0" { 207 wpf(w, "Status <b>DISabled</b><br><br>\n") 208 } else { 209 wpf(w, "Status <b>ENabled</b><br><br>\n") 210 } 211 212 imageRetrieve(w, r) 213 214 } 215 216 func foscamToggle(w http.ResponseWriter, r *http.Request, m map[string]interface{}) { 217 218 htmlfrag.SetNocacheHeaders(w) 219 w.Header().Set("Content-Type", "text/html; charset=utf-8") 220 221 ssecs := r.FormValue("sleep") 222 if ssecs != "" { 223 secs := util.Stoi(ssecs) 224 wpf(w, "sleeping %v secs ... <br><br>\n", secs) 225 time.Sleep(time.Duration(secs) * time.Second) 226 } 227 228 prevStat := makeRequest(w, r, path_get_alarm) 229 230 wpf(w, "||%s||<br>\n", prevStat.IsEnable) 231 if strings.TrimSpace(prevStat.IsEnable) == "0" { 232 prevStat.IsEnable = "1" 233 } else { 234 prevStat.IsEnable = "0" 235 } 236 prevStat.Area0 = "255" 237 prevStat.Area1 = "255" 238 prevStat.Area2 = "255" 239 prevStat.Area3 = "255" 240 prevStat.Area4 = "255" 241 prevStat.Area5 = "255" 242 prevStat.Area6 = "255" 243 prevStat.Area7 = "255" 244 prevStat.Area8 = "255" 245 prevStat.Area9 = "255" 246 247 // ugly: XML dump to query string 248 s2 := spf("%+v", prevStat) 249 s2 = strings.Trim(s2, "{}") 250 s2 = strings.Replace(s2, ":", "=", -1) 251 s2 = strings.Replace(s2, " ", "&", -1) 252 253 // even worse: we have to lower the case again 254 pairs := strings.Split(s2, "&") 255 recombined := "" 256 for i, v := range pairs { 257 fchar := v[:1] 258 fchar = strings.ToLower(fchar) 259 recombined += fchar + v[1:] 260 if i < len(pairs)-1 { 261 recombined += "&" 262 } 263 } 264 265 wpf(w, "<pre>") 266 // disS2 := stringspb.Breaker(s2, 50) 267 // for _, v := range disS2 { 268 // wpf(w, "%v\n", v) 269 // } 270 disRecombined := stringspb.Breaker(recombined, 50) 271 for _, v := range disRecombined { 272 wpf(w, "%v\n", v) 273 } 274 wpf(w, "</pre>") 275 // wpf(w, "<pre>%v</pre>\n", recombined) 276 277 toggleRes := makeRequest(w, r, path_set_alarm+"&"+recombined) 278 if toggleRes.Result == "0" { 279 wpf(w, "<br>end foscam toggle - success<br>\n") 280 if prevStat.IsEnable == "0" { 281 wpf(w, "<b>DISabled</b><br>\n") 282 } else { 283 wpf(w, "<b>ENabled</b><br>\n") 284 } 285 } 286 287 } 288 289 func foscamWatch(w http.ResponseWriter, r *http.Request, m map[string]interface{}) { 290 291 htmlfrag.SetNocacheHeaders(w) 292 w.Header().Set("Content-Type", "text/html; charset=utf-8") 293 294 wpf(w, tplx.ExecTplHelper(tplx.Head, map[string]interface{}{"HtmlTitle": "Foscam live watch"})) 295 296 /* 297 298 There is no way to access a real video stream. 299 Thus we use this suggestion: http://foscam.us/forum/post43654.html#p43654 300 301 */ 302 str := `<img 303 width='640' 304 src="http://` + dns_cam + `/CGIProxy.fcgi?cmd=snapPicture2&usr=visitor&pwd=visitor&t=" 305 onload='setTimeout(function() {src = src.substring(0, (src.lastIndexOf("t=")+2))+(new Date()).getTime()}, 1000)' 306 onerror='setTimeout(function() {src = src.substring(0, (src.lastIndexOf("t=")+2))+(new Date()).getTime()}, 5000)' 307 alt='' />` 308 w.Write([]byte(str)) 309 310 w.Write([]byte(tplx.Foot)) 311 312 } 313 314 func init() { 315 316 if util_appengine.IsLocalEnviron() { 317 dns_router = "192.168.1.1" 318 dns_cam = "192.168.1.4:8081" 319 } else { 320 dns_router = "ds7934.myfoscam.org" 321 dns_cam = "ds7934.myfoscam.org:8081" 322 } 323 324 http.HandleFunc("/foscam-status", loghttp.Adapter(foscamStatus)) 325 http.HandleFunc("/foscam-toggle", loghttp.Adapter(foscamToggle)) 326 http.HandleFunc("/foscam-watch", loghttp.Adapter(foscamWatch)) 327 328 }