├── LICENSE ├── README.md ├── example ├── auto_discovery │ ├── auto_discovery.py │ └── ssdp.py └── live_view.py └── lumix_control.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Michael Hand 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 | # python_lumix_control 2 | Python module for controlling Panasonic wifi enabled cameras. 3 | 4 | Super helpful info here: 5 | http://www.personal-view.com/talks/discussion/6703/control-your-gh3-from-a-web-browser-now-with-video-/p1 6 | 7 | SSDP Library for auto_discovery example from here: 8 | https://gist.github.com/dankrause/6000248 9 | -------------------------------------------------------------------------------- /example/auto_discovery/auto_discovery.py: -------------------------------------------------------------------------------- 1 | import lumix_control 2 | import ssdp 3 | 4 | discover = ssdp.discover('urn:schemas-upnp-org:service:ContentDirectory:1') 5 | IP = "" 6 | if len(discover): 7 | for response in discover: 8 | location = response.location 9 | IP = location.split(':60606')[0].replace('http://', '') # Have only tested this on my camera 10 | if IP != "": 11 | control = lumix_control.CameraControl(IP) -------------------------------------------------------------------------------- /example/auto_discovery/ssdp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Dan Krause 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import socket 16 | import httplib 17 | import StringIO 18 | 19 | class SSDPResponse(object): 20 | class _FakeSocket(StringIO.StringIO): 21 | def makefile(self, *args, **kw): 22 | return self 23 | def __init__(self, response): 24 | r = httplib.HTTPResponse(self._FakeSocket(response)) 25 | r.begin() 26 | self.location = r.getheader("location") 27 | self.usn = r.getheader("usn") 28 | self.st = r.getheader("st") 29 | self.cache = r.getheader("cache-control").split("=")[1] 30 | def __repr__(self): 31 | return "".format(**self.__dict__) 32 | 33 | def discover(service, timeout=5, retries=1, mx=3): 34 | group = ("239.255.255.250", 1900) 35 | message = "\r\n".join([ 36 | 'M-SEARCH * HTTP/1.1', 37 | 'HOST: {0}:{1}', 38 | 'MAN: "ssdp:discover"', 39 | 'ST: {st}','MX: {mx}','','']) 40 | socket.setdefaulttimeout(timeout) 41 | responses = {} 42 | for _ in range(retries): 43 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 44 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 45 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) 46 | sock.sendto(message.format(*group, st=service, mx=mx), group) 47 | while True: 48 | try: 49 | response = SSDPResponse(sock.recv(1024)) 50 | responses[response.location] = response 51 | except socket.timeout: 52 | break 53 | return responses.values() 54 | 55 | # Example: 56 | # import ssdp 57 | # ssdp.discover("roku:ecp") -------------------------------------------------------------------------------- /example/live_view.py: -------------------------------------------------------------------------------- 1 | import lumix_control 2 | import threading 3 | import subprocess 4 | 5 | #This gets more and more out of sync. 6 | 7 | IP = "10.0.1.105" #IP of camera 8 | control = lumix_control.CameraControl(IP) 9 | UDP_PORT = 5111 10 | 11 | def reload_stream(): 12 | control.start_stream(UDP_PORT) 13 | threading.Timer(10, reload_stream).start() # The stream times out after about 10 seconds. 14 | 15 | reload_stream() 16 | args = ['ffplay', '-v', 'quiet', 'udp://@:' + str(UDP_PORT)] #FFPlay handles all of the hard decoding stuff automatically 17 | subprocess.check_call(args) 18 | -------------------------------------------------------------------------------- /lumix_control.py: -------------------------------------------------------------------------------- 1 | import requests as r 2 | 3 | class CameraControl: 4 | def __init__(self, cam_ip): 5 | self.cam_ip = cam_ip 6 | self.baseurl = "http://{ip}/cam.cgi".format(ip=self.cam_ip) 7 | self.start_camera_control() 8 | 9 | def start_camera_control(self): 10 | resp = r.get(self.baseurl, params = {"mode": "camcmd", "value": "recmode"}) 11 | if self.check_response(resp): 12 | print ("Connected") 13 | 14 | def start_stream(self, upd_port): 15 | resp = r.get(self.baseurl, params = {"mode": "startstream", "value": str(upd_port)}) 16 | if self.check_response(resp): 17 | return True 18 | 19 | def stop_stream(self): 20 | resp = r.get(self.baseurl, params = {"mode": "stopstream"}) 21 | if self.check_response(resp): 22 | return True 23 | 24 | def get_info(self, setting): 25 | params = {"mode": "getinfo", "type": setting} 26 | resp = r.get(self.baseurl, params = params ) 27 | return resp 28 | 29 | def current_menu_info(self): 30 | resp = self.get_info("curmenu") 31 | return resp 32 | 33 | def all_menu_info(self): 34 | resp = self.get_info("allmenu") 35 | return resp 36 | 37 | def get_lens_info(self): 38 | # ?, max_fstop, min_fstop, max_shutter, min_shutter, ?, ?, max_zoom, min_zoom, ?, ?, ? 39 | resp = self.get_info("lens") 40 | return resp 41 | 42 | def get_setting(self, setting): 43 | params = {"mode": "getsetting", "type": setting} 44 | resp = r.get(self.baseurl, params = params ) 45 | return resp 46 | 47 | def get_focus_mode(self): 48 | resp = self.get_setting("focusmode") 49 | return resp 50 | 51 | def get_focus_mag(self): 52 | resp = self.get_setting("mf_asst_mag") 53 | return resp 54 | 55 | def get_mf_asst_setting(self): 56 | resp = self.get_setting("mf_asst") 57 | return resp 58 | 59 | def set_setting(self, settings): 60 | params = {"mode": "setsetting"} 61 | params.update(settings) 62 | resp = r.get(self.baseurl, params = params ) 63 | return resp 64 | 65 | def set_iso(self, ISO): 66 | if ISO == "auto": 67 | ISO = "50" 68 | resp = self.set_setting({"type": "iso", "value": ISO}) 69 | if self.check_response(resp): 70 | print ("ISO set to " + ISO) 71 | 72 | def set_focal(self, focal): 73 | # 256 between full stops. The rest are third stops. 74 | # See http://c710720.r20.cf2.rackcdn.com/wp-content/uploads/2011/08/ISO-Shutter-Speeds-Fstops-Copyright-2009-2011-photographyuncapped.gif 75 | fstop = { 76 | "1": "0/256", 77 | "1.1": "85/256", 78 | "1.2": "171/256", 79 | "1.4": "256/256", 80 | "1.6": "341/256", 81 | "1.8": "427/256", 82 | "2": "512/256", 83 | "2.2": "597/256", 84 | "2.4": "640/256", 85 | "2.8": "768/256", 86 | "3.2": "853/256", 87 | "3.5": "939/256", 88 | "4" : "1024/256", 89 | "4.5": "1110/256", 90 | "5": "1195/256", 91 | "5.6": "1280/256", 92 | "6.3": "1364/256", 93 | "7.1": "1451/256", 94 | "8": "1536/256", 95 | "9": "1621/256", 96 | "10": "1707/256", 97 | "11": "1792/256", 98 | "13": "1877/256", 99 | "14": "1963/256", 100 | "16": "2048/256", 101 | "18": "2133/256", 102 | "20": "2219/256", 103 | "22": "2304/256" 104 | } 105 | resp = self.set_setting({"type": "focal", "value": fstop[focal] }) 106 | if self.check_response(resp): 107 | print ("F Stop set to " + focal) 108 | 109 | def set_shutter(self, shutter): 110 | # 256 between full stops. 1 second is the pos/neg boundary 111 | # See http://c710720.r20.cf2.rackcdn.com/wp-content/uploads/2011/08/ISO-Shutter-Speeds-Fstops-Copyright-2009-2011-photographyuncapped.gif 112 | shutter_speed = { 113 | "4000": "3072/256", 114 | "3200": "2987/256", 115 | "2500": "2902/256", 116 | "2000": "2816/256", 117 | "1600": "2731/256", 118 | "1300": "2646/256", 119 | "1000": "2560/256", 120 | "800": "2475/256", 121 | "640": "2390/256", 122 | "500": "2304/256", 123 | "400": "2219/256", 124 | "320": "2134/256", 125 | "250": "2048/256", 126 | "200": "1963/256", 127 | "160": "1878/256", 128 | "125": "1792/256", 129 | "100": "1707/256", 130 | "80": "1622/256", 131 | "60": "1536/256", 132 | "50": "1451/256", 133 | "40": "1366/256", 134 | "30": "1280/256", 135 | "25": "1195/256", 136 | "20": "1110/256", 137 | "15": "1024/256", 138 | "13": "939/256", 139 | "10": "854/256", 140 | "8": "768/256", 141 | "6": "683/256", 142 | "5": "598/256", 143 | "4": "512/256", 144 | "3.2": "427/256", 145 | "2.5": "342/256", 146 | "2": "256/256", 147 | "1.6": "171/256", 148 | "1.3": "86/256", 149 | "1": "0/256", 150 | "1.3s": "-85/256", 151 | "1.6s": "-170/256", 152 | "2s": "-256/256", 153 | "2.5s": "-341/256", 154 | "3.2s": "-426/256", 155 | "4s": "-512/256", 156 | "5s": "-682/256", 157 | "6s": "-768/256", 158 | "8s": "-853/256", 159 | "10s": "-938/256", 160 | "13s": "-1024/256", 161 | "15s": "-1109/256", 162 | "20s": "-1194/256", 163 | "25s": "-1280/256", 164 | "30s": "-1365/256", 165 | "40s": "-1450/256", 166 | "50s": "-1536/256", 167 | "60s": "16384/256", 168 | "B": "256/256" 169 | } 170 | resp = self.set_setting({"type": "shtrspeed", "value": shutter_speed[shutter] }) 171 | if self.check_response(resp): 172 | print ("Shutter set to " + shutter) 173 | 174 | def set_video_quality(self, quality="mp4ed_30p_100mbps_4k"): 175 | # mp4_24p_100mbps_4k / mp4_30p_100mbps_4k 176 | resp = self.set_setting({"type": "videoquality", "value": quality}) 177 | if self.check_response(resp): 178 | print ("Video quality set to " + quality) 179 | return resp 180 | 181 | def focus_control(self, direction="tele", speed="normal"): 182 | #tele or wide for direction, normal or fast for speed 183 | params = {"mode": "camctrl", "type": "focus", "value": "{0}-{1}".format(direction, speed)} 184 | resp = r.get(self.baseurl, params = params ) 185 | return resp 186 | 187 | def rack_focus(self, start_point="current", end_point="0", speed="normal"): 188 | #tele or wide for direction, normal or fast for speed 189 | 190 | #Check where we are with a fine step 191 | resp = self.focus_control("tele", "normal").text 192 | current_position = int(resp.split(',')[1]) 193 | 194 | if end_point == "current": 195 | end_point = current_position + 13 196 | 197 | # First get to the starting point if necessary 198 | if start_point == "current": 199 | start_point = current_position + 13 200 | elif int(start_point) < current_position: 201 | while current_position - int(start_point) > 13: 202 | resp = self.focus_control("tele", "fast").text 203 | current_position = int(resp.split(',')[1]) 204 | else: 205 | while int(start_point) - current_position > 13: 206 | resp = self.focus_control("wide", "fast").text 207 | current_position = int(resp.split(',')[1]) 208 | 209 | #At the start, now let's get to the end point 210 | start_point = int(start_point) 211 | 212 | threshold = 13 213 | if speed == "fast": 214 | threshold = 70 215 | 216 | if start_point > int(end_point): 217 | while current_position - int(end_point) > threshold: 218 | resp = self.focus_control("tele", speed).text 219 | current_position = int(resp.split(',')[1]) 220 | if current_position - int(end_point) <= threshold: 221 | #Fine focus for the last bit 222 | threshold = 13 223 | speed = "normal" 224 | else: 225 | while int(end_point) - current_position > threshold: 226 | resp = self.focus_control("wide", speed).text 227 | current_position = int(resp.split(',')[1]) 228 | if int(end_point) - current_position <= threshold: 229 | threshold = 13 230 | speed = "normal" 231 | 232 | def capture_photo(self): 233 | params = {"mode": "camcmd", "value": "capture"} 234 | resp = r.get(self.baseurl, params = params) 235 | return resp 236 | 237 | def video_record_start(self): 238 | params = {"mode": "camcmd", "value": "video_recstart"} 239 | resp = r.get(self.baseurl, params = params) 240 | return resp 241 | 242 | def video_record_stop(self): 243 | params = {"mode": "camcmd", "value": "video_recstop"} 244 | resp = r.get(self.baseurl, params = params) 245 | return resp 246 | 247 | def check_response(self, resp): 248 | # Get a 200 response even on error. Have to check 249 | if "ok" in resp.text: 250 | return True 251 | else: 252 | print (resp.text) 253 | return False 254 | 255 | if __name__ == "__main__": 256 | IP = "10.0.1.105" 257 | control = CameraControl(IP) #IP of camera --------------------------------------------------------------------------------