Copy Python3 code to Python2 lib
Python2: - Added author - Added Sensor class - Added HomeSFR class - Corrected date error for cameras (datetime.datetime.strptime does not understand '0000-00-00 00:00:00') - Added encoding error TODO Python3: - Changed __name__ - Remove import of datetime.timezone - Corrected date error for cameras (datetime.datetime.strptime does not understand '0000-00-00 00:00:00')
This commit is contained in:
parent
7745571fcd
commit
d90da89b47
|
@ -1,19 +1,25 @@
|
||||||
# -*- coding: utf8 -*-
|
# -*- coding: utf8 -*-
|
||||||
'''
|
'''
|
||||||
Home by SFR wrapping class
|
Home by SFR wrapping class
|
||||||
Plain use of your Home by SFR device from a Python 3 library
|
Plain use of your Home by SFR device from a Python 2 library
|
||||||
|
|
||||||
Warning:
|
Warning:
|
||||||
This is a wrap aroud website, this could stop working without prior notice
|
This is a wrap aroud website, this could stop working without prior notice
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
## Write the library
|
## Manage cameras
|
||||||
|
### Get image
|
||||||
|
### Get video
|
||||||
|
## Manage logs
|
||||||
|
## Logout method
|
||||||
|
## Encoding error for sensors' names
|
||||||
|
|
||||||
authors = (
|
authors = (
|
||||||
|
'Gilles "Almtesh" Émilien MOREL',
|
||||||
)
|
)
|
||||||
name = 'homesfr for Python 2'
|
name = 'homesfr for Python 2'
|
||||||
version = '0.8-20160521'
|
version = '0.9-20160523'
|
||||||
|
|
||||||
# Settable modes
|
# Settable modes
|
||||||
MODE_OFF = 0
|
MODE_OFF = 0
|
||||||
|
@ -27,4 +33,365 @@ SMOKE_DETECTOR = 'SMOKE'
|
||||||
SIREN = 'SIREN'
|
SIREN = 'SIREN'
|
||||||
REMOTE_CONTROLER = 'REMOTE'
|
REMOTE_CONTROLER = 'REMOTE'
|
||||||
KEYPAD_CONTROLER = 'KEYPAD'
|
KEYPAD_CONTROLER = 'KEYPAD'
|
||||||
PRESENCE_CAMERA_DETECTOR = 'PIR_CAMERA'
|
PRESENCE_CAMERA_DETECTOR = 'PIR_CAMERA'
|
||||||
|
|
||||||
|
from cookielib import CookieJar
|
||||||
|
import urllib2 as request
|
||||||
|
from urllib2 import HTTPError
|
||||||
|
from urllib import urlencode
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def bytes2file (b):
|
||||||
|
'''
|
||||||
|
Gives a file-like class from a Bytes
|
||||||
|
'''
|
||||||
|
from io import BytesIO
|
||||||
|
r = BytesIO ()
|
||||||
|
r.write (b)
|
||||||
|
r.seek (0)
|
||||||
|
return (r)
|
||||||
|
|
||||||
|
def bytes2image (b):
|
||||||
|
'''
|
||||||
|
Gives a Image object from bytes
|
||||||
|
Uses the bytes2file function
|
||||||
|
'''
|
||||||
|
from PIL import Image
|
||||||
|
f = bytes2file (b)
|
||||||
|
r = Image ()
|
||||||
|
r.open (f)
|
||||||
|
return (r)
|
||||||
|
|
||||||
|
class HomeSFR ():
|
||||||
|
def __init__ (self, username = None, password = None, cookies = None, debug = False, autologin = True):
|
||||||
|
'''
|
||||||
|
Sets the class with username and password couple, or cookies
|
||||||
|
Both user/password and cookies can be set, the cookies will be used first
|
||||||
|
The debug parameter defines if the class will write debug messages to stdout, if False, the stdout will never be writen by the class
|
||||||
|
The autologin parameter defines if the class will manage the login by itself, if False, the user must call login () to login and test_login () to check the login
|
||||||
|
The autologin paramater will always be False if no username and password are defined, and the login () method will always return False
|
||||||
|
'''
|
||||||
|
self.DEBUG = debug
|
||||||
|
if self.DEBUG:
|
||||||
|
print (name + ' ' + version)
|
||||||
|
print ('Authors:')
|
||||||
|
for i in authors:
|
||||||
|
print (' - ' + i)
|
||||||
|
if username != None:
|
||||||
|
print ('init with username ' + username)
|
||||||
|
if cookies != None:
|
||||||
|
print ('init with cookies')
|
||||||
|
print ('debug = ' + str (debug))
|
||||||
|
print ('autologin = ' + str (autologin))
|
||||||
|
|
||||||
|
if (username == None or password == None) and cookies == None:
|
||||||
|
raise TypeError ('You must define either username AND password or cookies')
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
if self.username != None and self.password != None:
|
||||||
|
self.autologin = autologin
|
||||||
|
else:
|
||||||
|
self.autologin = False
|
||||||
|
if cookies == None:
|
||||||
|
self.cookies = CookieJar ()
|
||||||
|
else:
|
||||||
|
self.cookies = cookies
|
||||||
|
self.opener = request.build_opener (request.HTTPCookieProcessor (self.cookies))
|
||||||
|
|
||||||
|
# Specific configuration
|
||||||
|
self.base_url = 'https://home.sfr.fr'
|
||||||
|
|
||||||
|
# path to login test
|
||||||
|
self.auth_path = '/mysensors'
|
||||||
|
self.auth_ok = '/accueil' # if logged
|
||||||
|
self.auth_post_url = 'https://boutique.home.sfr.fr/authentification'
|
||||||
|
self.auth_referer = 'https://boutique.home.sfr.fr/authentification?back=service'
|
||||||
|
self.auth_user_field = 'email'
|
||||||
|
self.auth_pass_field = 'passwd'
|
||||||
|
self.auth_extra_fields = {'back': 'service', 'token_sso': '', 'error_sso': '', 'SubmitLogin': 'OK'}
|
||||||
|
|
||||||
|
# Path to sensors and mode
|
||||||
|
self.sensors_list = '/mysensors'
|
||||||
|
self.sensors_label = 'Sensor'
|
||||||
|
self.sensors_label_id = 'id'
|
||||||
|
|
||||||
|
# Path to list of alerts
|
||||||
|
self.alerts_path = '/listalert'
|
||||||
|
|
||||||
|
# Path to get and set modes
|
||||||
|
self.mode_get_path = '/mysensors'
|
||||||
|
self.mode_get_label = 'alarm_mode'
|
||||||
|
self.mode_set_path = '/alarmmode'
|
||||||
|
self.mode_set_field = 'action' # Name for GET field
|
||||||
|
self.mode_off = 'OFF' # Value for off
|
||||||
|
self.mode_custom = 'CUSTOM' # Value for custom
|
||||||
|
self.mode_on = 'ON' # Value for on
|
||||||
|
|
||||||
|
# Cameras
|
||||||
|
# mac=00:0e:8f:c9:59:44&flip=0&led_mode=0&alert_pan=1&rec24=0&da=1&dp=4&name=Salon
|
||||||
|
self.cameras_list = '/homescope/mycams'
|
||||||
|
self.camera_snapshot = '/homescope/snapshot'
|
||||||
|
self.camera_video = '/homescope/flv'
|
||||||
|
self.camera_get_config_path = '/homescope/camsettings'
|
||||||
|
self.camera_set_config_path = '/homescope/camsettings'
|
||||||
|
self.camera_set_config_mac = 'mac'
|
||||||
|
self.camera_set_config_flip = 'flip'
|
||||||
|
self.camera_set_config_leds = 'led_mode' # set to 0 to turn the leds on
|
||||||
|
self.camera_set_config_detectionsensibility = 'dp' # from 1 to 4,
|
||||||
|
|
||||||
|
def __str__ (self):
|
||||||
|
'''
|
||||||
|
Shows name, version, defined user and debug state
|
||||||
|
'''
|
||||||
|
if self.username != None:
|
||||||
|
return (name + ' ' + version + '\nUser: ' + self.username + '\nDebug: ' + str (self.DEBUG))
|
||||||
|
else:
|
||||||
|
return (name + ' ' + version + '\nUser: Unknown (auth from cookie class)\nDebug: ' + str (self.DEBUG))
|
||||||
|
|
||||||
|
def test_login (self):
|
||||||
|
'''
|
||||||
|
Tests if the client is logged
|
||||||
|
Return True if it's logged, returns False either
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Testing login')
|
||||||
|
self.opener.open (self.base_url + self.auth_path)
|
||||||
|
except HTTPError:
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Not connected')
|
||||||
|
return (False)
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Connected')
|
||||||
|
return (True)
|
||||||
|
|
||||||
|
def login (self):
|
||||||
|
'''
|
||||||
|
Logs in the HomeBySFR service
|
||||||
|
Call this function first or exception will be raised
|
||||||
|
Returns True if the login was a success, False either
|
||||||
|
This method will always return False if no username and password are defined
|
||||||
|
'''
|
||||||
|
if self.username != None and self.password != None:
|
||||||
|
self.opener.open (self.auth_referer)
|
||||||
|
data = self.auth_extra_fields
|
||||||
|
data [self.auth_user_field] = self.username
|
||||||
|
data [self.auth_pass_field] = self.password
|
||||||
|
data = bytes (urlencode (data))
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Cookies ' + str( len(self.cookies)))
|
||||||
|
print ('Sending data ' + str (data))
|
||||||
|
a = self.opener.open (self.auth_post_url, data = data)
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Auth redirected to ' + a.geturl ())
|
||||||
|
return (a.geturl () == self.base_url + self.auth_ok)
|
||||||
|
else:
|
||||||
|
return (False)
|
||||||
|
|
||||||
|
def set_mode (self, mode):
|
||||||
|
'''
|
||||||
|
Sets the detection mode
|
||||||
|
For the current version, use the MODE_OFF, MODE_ON and MODE_CUSTOM constants and always returns True, but raises an exception if a problem happens
|
||||||
|
'''
|
||||||
|
if (self.autologin and self.test_login () == False):
|
||||||
|
self.login ()
|
||||||
|
if mode == MODE_OFF:
|
||||||
|
m = self.mode_off
|
||||||
|
elif mode == MODE_CUSTOM:
|
||||||
|
m = self.mode_custom
|
||||||
|
elif mode == MODE_ON:
|
||||||
|
m = self.mode_on
|
||||||
|
else:
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('You should use the MODE_OFF, MODE_ON and MODE_CUSTOM constants to set this.')
|
||||||
|
raise ValueError
|
||||||
|
r = self.base_url + self.mode_set_path + '?' + self.mode_set_field + '=' + m
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Will get ' + r)
|
||||||
|
self.opener.open (r)
|
||||||
|
return (True)
|
||||||
|
|
||||||
|
def get_mode (self):
|
||||||
|
'''
|
||||||
|
Gets the detection mode
|
||||||
|
Returns one of MODE_OFF, MODE_ON and MODE_CUSTOM constants, or None if something went wrong
|
||||||
|
'''
|
||||||
|
if (self.autologin and self.test_login () == False):
|
||||||
|
self.login ()
|
||||||
|
r = self.base_url + self.mode_get_path
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Getting ' + r)
|
||||||
|
a = bytes2file (self.opener.open (r).read ())
|
||||||
|
b = ET.parse (a).getroot ()
|
||||||
|
c = b.get (self.mode_get_label)
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Got mode ' + c)
|
||||||
|
if (c == self.mode_off):
|
||||||
|
return (MODE_OFF)
|
||||||
|
if (c == self.mode_custom):
|
||||||
|
return (MODE_CUSTOM)
|
||||||
|
if (c == self.mode_on):
|
||||||
|
return (MODE_ON)
|
||||||
|
return (None)
|
||||||
|
|
||||||
|
def list_sensors (self):
|
||||||
|
'''
|
||||||
|
Returns a list of sensors' ids.
|
||||||
|
'''
|
||||||
|
if (self.autologin and self.test_login () == False):
|
||||||
|
self.login ()
|
||||||
|
r = self.base_url + self.sensors_list
|
||||||
|
a = bytes2file (self.opener.open (r).read ())
|
||||||
|
b = ET.parse (a)
|
||||||
|
r = []
|
||||||
|
for i in b.findall (self.sensors_label):
|
||||||
|
r.append (i.get (self.sensors_label_id))
|
||||||
|
return (list (r))
|
||||||
|
|
||||||
|
def get_sensor (self, id):
|
||||||
|
'''
|
||||||
|
Returns a Sensor object for the sensor id or None if sensor is not found
|
||||||
|
The available ids can be got from the list_sensors method
|
||||||
|
'''
|
||||||
|
def build_tree (element):
|
||||||
|
r = {}
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Diving in the element ' + element.tag)
|
||||||
|
for i in element.getchildren ():
|
||||||
|
if i.getchildren () == []:
|
||||||
|
r.update ({i.tag: i.text})
|
||||||
|
else:
|
||||||
|
r.update ({i.tag: build_tree (i)})
|
||||||
|
return (r)
|
||||||
|
if (self.autologin and self.test_login () == False):
|
||||||
|
self.login ()
|
||||||
|
r = self.base_url + self.sensors_list
|
||||||
|
a = bytes2file (self.opener.open (r).read ())
|
||||||
|
b = ET.parse (a)
|
||||||
|
r = None
|
||||||
|
for i in b.findall (self.sensors_label):
|
||||||
|
if self.DEBUG:
|
||||||
|
print ('Testing sensors ' + i.get (self.sensors_label_id))
|
||||||
|
if (i.get (self.sensors_label_id) == id):
|
||||||
|
r = build_tree (i)
|
||||||
|
break
|
||||||
|
return (Sensor (r))
|
||||||
|
|
||||||
|
def get_all_sensors (self):
|
||||||
|
'''
|
||||||
|
Returns a tuple of sensors as described in the get_sensor method
|
||||||
|
'''
|
||||||
|
r = []
|
||||||
|
for i in self.list_sensors ():
|
||||||
|
r.append (self.get_sensor (i))
|
||||||
|
return (tuple (r))
|
||||||
|
|
||||||
|
def get_cookies (self):
|
||||||
|
'''
|
||||||
|
Returns the CookieJar as it is now, for further use
|
||||||
|
It's strongly recommended to use this method only before a object delete
|
||||||
|
'''
|
||||||
|
return (self.cookies)
|
||||||
|
|
||||||
|
class Sensor:
|
||||||
|
'''
|
||||||
|
Class used to read easily the sensors
|
||||||
|
'''
|
||||||
|
def __init__ (self, sensor_dict):
|
||||||
|
'''
|
||||||
|
Initialize the class with the dict producted by HomeSFR.get_sensors ()
|
||||||
|
'''
|
||||||
|
|
||||||
|
self.sensor_dict = sensor_dict
|
||||||
|
|
||||||
|
# Field names
|
||||||
|
self.type_field = 'deviceType'
|
||||||
|
self.model_field = 'deviceModel'
|
||||||
|
self.version_field = 'deviceVersion'
|
||||||
|
self.name_field = 'name'
|
||||||
|
self.longname_field = 'long_name'
|
||||||
|
self.namegender_field = 'name_gender' # Only usefull for French for the moment
|
||||||
|
self.batterylevel_field = 'batteryLevel'
|
||||||
|
self.signal_field = 'signalLevel'
|
||||||
|
self.lasttrigger_field = 'lastTriggerTime'
|
||||||
|
self.lasttrigger_dateformat = '%Y-%m-%d %H:%M:%S'
|
||||||
|
self.status_field = 'status'
|
||||||
|
self.status_value_ok = 'OK'
|
||||||
|
# I don't have any other value for the moment
|
||||||
|
|
||||||
|
def get_raw (self):
|
||||||
|
'''
|
||||||
|
Returns the raw dict, as presented in the original XML file
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict)
|
||||||
|
|
||||||
|
def get_type (self):
|
||||||
|
'''
|
||||||
|
Returns the sensor's type
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict [self.type_field])
|
||||||
|
|
||||||
|
def get_model (self):
|
||||||
|
'''
|
||||||
|
Returns the sensor's model, if any, None either
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict [self.model_field])
|
||||||
|
|
||||||
|
def get_version (self):
|
||||||
|
'''
|
||||||
|
Returns the sensor's version
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict [self.version_field])
|
||||||
|
|
||||||
|
def get_name (self):
|
||||||
|
'''
|
||||||
|
Returns the sensor's name
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict [self.name_field])
|
||||||
|
|
||||||
|
def get_longname (self):
|
||||||
|
'''
|
||||||
|
Returns the sensor's type name in system's language and the sensor's name
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict [self.longname_field])
|
||||||
|
|
||||||
|
def get_namegender (self):
|
||||||
|
'''
|
||||||
|
Return M for male and F for female.
|
||||||
|
Only usefull for languages with gender on nouns
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict [self.namegender_field])
|
||||||
|
|
||||||
|
def get_batterylevel (self):
|
||||||
|
'''
|
||||||
|
Returns the sensor's battery level, out of 10
|
||||||
|
It seems that batteryless sensors return 255
|
||||||
|
'''
|
||||||
|
return (int (self.sensor_dict [self.batterylevel_field]))
|
||||||
|
|
||||||
|
def get_signal (self):
|
||||||
|
'''
|
||||||
|
Returns the sensor's signal quality, out of 10
|
||||||
|
'''
|
||||||
|
return (int (self.sensor_dict [self.signal_field]))
|
||||||
|
|
||||||
|
def get_lasttrigger (self):
|
||||||
|
'''
|
||||||
|
Return the timestamp of the sensor's last triger
|
||||||
|
The sensors always trigger, even when the alarm's mode is off
|
||||||
|
'''
|
||||||
|
a = self.sensor_dict [self.lasttrigger_field]
|
||||||
|
# Try because camera return the date '0000-00-00 00:00:00' that is ununderstandable
|
||||||
|
try:
|
||||||
|
b = datetime.strptime (a, self.lasttrigger_dateformat)
|
||||||
|
except ValueError:
|
||||||
|
return (0)
|
||||||
|
r = int (b.strftime ('%s'))
|
||||||
|
return (r)
|
||||||
|
|
||||||
|
def get_status (self):
|
||||||
|
'''
|
||||||
|
Returns True is the sensor is OK, False either
|
||||||
|
'''
|
||||||
|
return (self.sensor_dict [self.status_field] == self.status_value_ok)
|
|
@ -16,7 +16,7 @@ This is a wrap aroud website, this could stop working without prior notice
|
||||||
authors = (
|
authors = (
|
||||||
'Gilles "Almtesh" Émilien MOREL',
|
'Gilles "Almtesh" Émilien MOREL',
|
||||||
)
|
)
|
||||||
name = 'pyhomesfr'
|
name = 'homesfr for Python 3'
|
||||||
version = '0.9-20160523'
|
version = '0.9-20160523'
|
||||||
|
|
||||||
# Settable modes
|
# Settable modes
|
||||||
|
@ -38,7 +38,7 @@ from http.cookiejar import CookieJar
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
|
|
||||||
def bytes2file (b):
|
def bytes2file (b):
|
||||||
'''
|
'''
|
||||||
|
@ -380,7 +380,11 @@ class Sensor:
|
||||||
The sensors always trigger, even when the alarm's mode is off
|
The sensors always trigger, even when the alarm's mode is off
|
||||||
'''
|
'''
|
||||||
a = self.sensor_dict [self.lasttrigger_field]
|
a = self.sensor_dict [self.lasttrigger_field]
|
||||||
b = datetime.strptime (a, self.lasttrigger_dateformat)
|
# Try because camera return the date '0000-00-00 00:00:00' that is ununderstandable
|
||||||
|
try:
|
||||||
|
b = datetime.strptime (a, self.lasttrigger_dateformat)
|
||||||
|
except ValueError:
|
||||||
|
return (0)
|
||||||
r = int (b.timestamp ())
|
r = int (b.timestamp ())
|
||||||
return (r)
|
return (r)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue