200 lines
7.5 KiB
Python
Executable File
200 lines
7.5 KiB
Python
Executable File
# Backup plugin for Domoticz
|
||
#
|
||
# Author: fjumelle
|
||
#
|
||
"""
|
||
<plugin key="DomoticzBackup" name="Domoticz Backup" author="fjumelle" version="1.0.0" wikilink="" externallink="">
|
||
<description>
|
||
<h2>Domoticz Backup</h2><br/>
|
||
Backup the Domoticz configuration (Database, plugins, scripts).<br/>
|
||
</description>
|
||
<params>
|
||
<param field="Address" label="Domoticz IP Address" width="200px" required="true" default="127.0.0.1"/>
|
||
<param field="Port" label="Port" width="40px" required="true" default="8080"/>
|
||
<param field="Username" label="Domoticz Username" width="200px" required="false" default=""/>
|
||
<param field="Password" label="Domoticz Password" width="200px" required="false" default=""/>
|
||
<param field="Mode1" label="Destination" width="400px" required="true" default=""/>
|
||
<param field="Mode2" label="Cleanup (days)" width="40px" required="true" default="30"/>
|
||
<param field="Mode6" label="Logging Level" width="200px">
|
||
<options>
|
||
<option label="Normal" value="0" default="true"/>
|
||
<option label="Verbose" value="1"/>
|
||
</options>
|
||
</param>
|
||
</params>
|
||
</plugin>
|
||
"""
|
||
import Domoticz
|
||
import os
|
||
import pwd
|
||
import grp
|
||
import shutil
|
||
import json
|
||
import zipfile
|
||
|
||
import urllib.parse as parse
|
||
import urllib.request as request
|
||
|
||
from datetime import datetime, timedelta
|
||
|
||
DATE_FORMAT = "%Y%m%d%H%M%S"
|
||
|
||
class BasePlugin:
|
||
def __init__(self):
|
||
return
|
||
|
||
def onStart(self):
|
||
# setup the appropriate logging level
|
||
debuglevel = int(Parameters["Mode6"])
|
||
if debuglevel != 0:
|
||
Domoticz.Debugging(debuglevel)
|
||
else:
|
||
Domoticz.Debugging(0)
|
||
|
||
#Heartbeat
|
||
Domoticz.Heartbeat(30)
|
||
|
||
# create the child devices if these do not exist yet
|
||
if 1 not in Devices:
|
||
Domoticz.Device(Name="Domoticz Backup", Unit=1, Image=9, TypeName="Switch", Used=1).Create()
|
||
Devices[1].Update(nValue=0, sValue="Off")
|
||
|
||
def onCommand(self, Unit, Command, Level, Hue):
|
||
if Unit == 1:
|
||
if Command == "On":
|
||
destination = Parameters["Mode1"]
|
||
cleanup = Parameters["Mode2"]
|
||
domoticz_host = Parameters["Address"]
|
||
domoticz_port = Parameters["Port"]
|
||
domoticz_user = Parameters["Username"]
|
||
domoticz_pass = Parameters["Password"]
|
||
backupDomoticz(domoticz_host, domoticz_port, domoticz_user, domoticz_pass, destination, cleanup)
|
||
|
||
#switch back to "Off"
|
||
Devices[1].Update(nValue=0, sValue="Off")
|
||
|
||
global _plugin
|
||
_plugin = BasePlugin()
|
||
|
||
def onStart():
|
||
global _plugin
|
||
_plugin.onStart()
|
||
|
||
def onCommand(Unit, Command, Level, Hue):
|
||
global _plugin
|
||
_plugin.onCommand(Unit, Command, Level, Hue)
|
||
|
||
def onHeartbeat():
|
||
pass
|
||
|
||
def backupDomoticz(host, port, user, password, destination, cleanup):
|
||
Domoticz.Status("Backup Domoticz files (plugins, scripts, db, ...)")
|
||
try:
|
||
current_time = datetime.now().strftime(DATE_FORMAT)
|
||
|
||
#Create current_time folder
|
||
work_folder = os.path.join(destination, current_time)
|
||
if not os.path.exists(work_folder):
|
||
os.mkdir(work_folder)
|
||
|
||
try:
|
||
#Domoticz root folder
|
||
domoticz_root = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", ".."))
|
||
|
||
#Archive plugins folder
|
||
archiveFolder(work_folder, os.path.join(domoticz_root, "plugins"))
|
||
#Archive scripts folder
|
||
archiveFolder(work_folder, os.path.join(domoticz_root, "scripts"))
|
||
#Archive WWW folder
|
||
if int(current_time[6:8]) in (1, 11, 21):
|
||
archiveFolder(work_folder, os.path.join(domoticz_root, "www"))
|
||
#Copy DB
|
||
archiveDatabase(host, port, user, password, work_folder)
|
||
|
||
#Delete obsolete archives
|
||
deleteOldBackups(destination, cleanup)
|
||
|
||
#Change owner
|
||
chown(work_folder)
|
||
|
||
except:
|
||
#Change owner
|
||
chown(work_folder)
|
||
raise
|
||
except Exception as exc:
|
||
Domoticz.Error("Error during Domoticz backup!")
|
||
Domoticz.Error(str(exc))
|
||
#Send notification
|
||
subject = "Error during Domoticz backup"
|
||
body = str(exc)
|
||
DomoticzAPI("type=command¶m=sendnotification&subject={}&body={}".format(subject, body))
|
||
|
||
def archiveFolder(archive_folder, folder):
|
||
folder = os.path.abspath(folder)
|
||
root_folder = os.path.abspath(os.path.join(folder, ".."))
|
||
archive = os.path.abspath(os.path.join(archive_folder, os.path.basename(folder) + ".zip"))
|
||
Domoticz.Status("Archive folder {} in {}".format(folder, archive))
|
||
ziph = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED)
|
||
for root, dirs, files in os.walk(folder):
|
||
for current_file in files:
|
||
file_to_arch = os.path.abspath(os.path.join(root, current_file))
|
||
file_in_arc = file_to_arch.replace(root_folder, ".")
|
||
ziph.write(file_to_arch, file_in_arc)
|
||
ziph.close()
|
||
|
||
def archiveDatabase(host, port, user, password, archive_folder):
|
||
db_dst = os.path.join(archive_folder, "domoticz.db")
|
||
Domoticz.Status("Archive database in {}".format(db_dst))
|
||
url = "http://{}:{}/backupdatabase.php".format(host, port)
|
||
request.urlretrieve(url, db_dst)
|
||
ziph = zipfile.ZipFile(db_dst + ".zip", 'w', zipfile.ZIP_DEFLATED)
|
||
ziph.write(db_dst, os.path.basename(db_dst))
|
||
os.remove(db_dst)
|
||
|
||
def deleteOldBackups(root_archive_folder, cleanup):
|
||
for dir in os.listdir(root_archive_folder):
|
||
try:
|
||
dir_date = datetime.strptime(dir, DATE_FORMAT)
|
||
if dir_date < datetime.now() - timedelta(days=int(cleanup)):
|
||
folder = os.path.join(root_archive_folder, dir)
|
||
Domoticz.Status("Remove backup '{}'".format(folder))
|
||
chown(folder)
|
||
shutil.rmtree(folder, ignore_errors=True)
|
||
except:
|
||
pass
|
||
|
||
def chown(folder):
|
||
uid = pwd.getpwnam("nobody").pw_uid
|
||
gid = grp.getgrnam("nogroup").gr_gid
|
||
os.chown(folder, uid, gid)
|
||
for root, dirs, files in os.walk(folder):
|
||
for momo in dirs:
|
||
os.chown(os.path.join(root, momo), uid, gid)
|
||
for momo in files:
|
||
os.chown(os.path.join(root, momo), uid, gid)
|
||
|
||
def DomoticzAPI(APICall):
|
||
resultJson = None
|
||
url = "http://{}:{}/json.htm?{}".format(Parameters["Address"], Parameters["Port"], parse.quote(APICall, safe="&="))
|
||
Domoticz.Debug("Calling domoticz API: {}".format(url))
|
||
try:
|
||
req = request.Request(url)
|
||
if Parameters["Username"] != "":
|
||
Domoticz.Debug("Add authentification for user {}".format(Parameters["Username"]))
|
||
credentials = ('%s:%s' % (Parameters["Username"], Parameters["Password"]))
|
||
encoded_credentials = base64.b64encode(credentials.encode('ascii'))
|
||
req.add_header('Authorization', 'Basic %s' % encoded_credentials.decode("ascii"))
|
||
|
||
response = request.urlopen(req)
|
||
if response.status == 200:
|
||
resultJson = json.loads(response.read().decode('utf-8'))
|
||
if resultJson["status"] != "OK":
|
||
Domoticz.Error("Domoticz API returned an error: status = {}".format(resultJson["status"]))
|
||
resultJson = None
|
||
else:
|
||
Domoticz.Error("Domoticz API: http error = {}".format(response.status))
|
||
except Exception as err:
|
||
Domoticz.Error("Error calling '{}'".format(url))
|
||
Domoticz.Error(str(err))
|
||
return resultJson
|