Skip to content

Commit

Permalink
initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
giejay committed Nov 24, 2018
0 parents commit 938c9cf
Show file tree
Hide file tree
Showing 18 changed files with 515 additions and 0 deletions.
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## 0.0.1
- Initial setup
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Kappelt gBridge - Domoticz Python Plugin
Python plugin for Domoticz to add integration with [gBridge](https://github.com/kservices/gBridge) project

## Prerequisites

Setup and run gBridge server, for now only local setups are supported due to username/password protection of remote MQTT server (https://doc.gbridge.kappelt.net/html/index.html).

## Installation

1. Clone repository into your domoticz plugins folder
```
cd domoticz/plugins
git clone --- gbridge
```
2. Restart domoticz
3. Go to "Hardware" page and add new item with type "gBridge"
4. Set your MQTT server address, gBridge config etc. to plugin settings

Once plugin receive any message from mqtt server it will call the domoticz API and switch on a light/switch or change brightness of a device

## Plugin update

1. Stop domoticz
2. Go to plugin folder and pull new version
```
cd domoticz/plugins/gbridge
git pull
```
3. Start domoticz

## Supported devices

For now only the onoff and brightness traits are supported
14 changes: 14 additions & 0 deletions adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from adapters.dimmable_adapter import DimmableAdapter
from adapters.on_off_switch_adapter import OnOffSwitchAdapter
from adapters.scene_adapter import SceneAdapter

adapter_by_type = {
'brightness': {
'Default': DimmableAdapter()
},
'onoff': {
'Default': OnOffSwitchAdapter(),
'Scene': SceneAdapter(),
'Group': SceneAdapter()
}
}
Binary file added adapters/__pycache__/__init__.cpython-35.pyc
Binary file not shown.
Binary file added adapters/__pycache__/base_adapter.cpython-35.pyc
Binary file not shown.
Binary file added adapters/__pycache__/dimmable_adapter.cpython-35.pyc
Binary file not shown.
Binary file not shown.
Binary file added adapters/__pycache__/scene_adapter.cpython-35.pyc
Binary file not shown.
18 changes: 18 additions & 0 deletions adapters/base_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import urllib.parse
import urllib.request
import Domoticz

class Adapter:

def __init__(self):
pass

def handleMqttMessage(self, device_id, data, domoticz_port):
pass

def callDomoticzApi(self, domoticz_port, command):
url = "http://localhost:%s/json.htm?type=command&%s" % (domoticz_port, command)
Domoticz.Debug('Executing command for device: %s' % command)
req = urllib.request.Request(url)
response = urllib.request.urlopen(req).read()
return response.decode('utf-8')
13 changes: 13 additions & 0 deletions adapters/dimmable_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from adapters.base_adapter import Adapter
import urllib.parse

class DimmableAdapter(Adapter):

def handleMqttMessage(self, device_id, data, domoticz_port):
params = {
'param': 'switchlight',
'idx': device_id,
'switchcmd': 'Set Level',
'level': data
}
Adapter.callDomoticzApi(self, domoticz_port, urllib.parse.urlencode(params))
23 changes: 23 additions & 0 deletions adapters/on_off_switch_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from adapters.base_adapter import Adapter
import urllib.parse

class OnOffSwitchAdapter(Adapter):

def __init__(self):
Adapter.__init__(self)

def handleMqttMessage(self, device_id, data, domoticz_port):
if data == '1':
command = 'On'
else:
command = 'Off'

params = {
'param': self.getType(),
'idx': device_id,
'switchcmd': command
}
Adapter.callDomoticzApi(self, domoticz_port, urllib.parse.urlencode(params))

def getType(self):
return 'switchlight'
6 changes: 6 additions & 0 deletions adapters/scene_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from adapters.on_off_switch_adapter import OnOffSwitchAdapter

class SceneAdapter(OnOffSwitchAdapter):

def getType(self):
return 'switchscene'
31 changes: 31 additions & 0 deletions domoticz_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Domoticz
import json
import re
import urllib.parse
import urllib.request

class DomoticzClient:
DomoticzPort = ""

def __init__(self, domoticz_port):
self.DomoticzPort = domoticz_port
Domoticz.Debug("gBridgeClient::__init__")

def fetchDevicesFromDomoticz(self):
req = urllib.request.Request(
"http://localhost:%d/json.htm?type=devices&filter=all&used=true&order=Name" % self.DomoticzPort)
response = urllib.request.urlopen(req).read().decode('utf-8')
Domoticz.Debug("Fetching all devices from Domoticz %s" % response)
return json.loads(response)['result']

def getDevicesByName(self, domoticz_devices):
domoticz_devices_by_name = {}
for device in domoticz_devices:
if "gBridge" in device['Description']:
match = re.search('gBridge:(.*)', device['Description'])
if match:
name = match.group(1)
else:
name = device['Name']
domoticz_devices_by_name[name] = device
return domoticz_devices_by_name
73 changes: 73 additions & 0 deletions gbridge_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Domoticz
import json
import urllib.parse
import urllib.request
from base64 import b64encode


class gBridgeClient:
Address = ""
Username = ""
Password = ""

brightness_devices = ['Blinds Percentage', 'Dimmer', 'Blinds Percentage Inverted']

def __init__(self, address, username, password):
Domoticz.Debug("gBridgeClient::__init__")
self.Address = address
self.Username = username
self.Password = password

def syncDevices(self, domoticz_devices_by_name, bridge_devices, delete_removed_devices):
bridge_devices_by_name = {x['name']: x for x in bridge_devices}

# Add devices which are not in gBridge yet
for name, device in domoticz_devices_by_name.items():
if name not in bridge_devices_by_name:
# Switch by default
type = 3
# On/Off trait
traits = [1]

if 'SwitchType' in device and device['SwitchType'] in self.brightness_devices:
# Brightness trait
traits.append(2)
# Light
type = 1
self.createDevice(name, type, traits)

# remove devices in gbridge which are no longer in domoticz
if delete_removed_devices:
for device in bridge_devices:
if device['name'] not in domoticz_devices_by_name:
self.deleteDevice(device['device_id'])

def fetchDevicesFromBridge(self):
url = "%s/api/device" % self.Address
req = urllib.request.Request(url)
req.add_header("Authorization", 'Basic %s' % self.getAuthHeader())
response = urllib.request.urlopen(req).read().decode('utf-8')
Domoticz.Debug("Fetching all devices from gBridge %s" % response)
return json.loads(response)

def deleteDevice(self, id):
gBridgeUrl = "%s/api/device/%s" % (self.Address, id)
req = urllib.request.Request(gBridgeUrl, method='DELETE')
req.add_header("Authorization", 'Basic %s' % self.getAuthHeader())
response = str(urllib.request.urlopen(req).read())
Domoticz.Debug('Delete device %s which is no longer in Domoticz, response: %s' % (id, response))
return response == b'Created'

def createDevice(self, name, type, traits):
values = {'name': name, 'type': type, 'traits': traits, 'topicPrefix': name}
data = json.dumps(values).encode('ascii')
req = urllib.request.Request("%s/api/device" % self.Address, data)
req.add_header('Content-Type', 'application/json')
req.add_header("Authorization", 'Basic %s' % self.getAuthHeader())
response = str(urllib.request.urlopen(req).read())
Domoticz.Debug(
'Try to create device %s for type %s with traits %s, response: %s' % (name, type, str(traits), response))
return response == b'Created'

def getAuthHeader(self):
return b64encode(bytes("%s:%s" % (self.Username, self.Password), 'utf-8')).decode("ascii")
126 changes: 126 additions & 0 deletions mqtt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Based on https://github.com/emontnemery/domoticz_mqtt_discovery
import Domoticz
import time
import json

class MqttClient:
Address = ""
Port = ""
mqttConn = None
isConnected = False
mqttConnectedCb = None
mqttDisconnectedCb = None
mqttPublishCb = None

def __init__(self, destination, port, mqttConnectedCb, mqttDisconnectedCb, mqttPublishCb, mqttSubackCb):
Domoticz.Debug("MqttClient::__init__")
self.Address = destination
self.Port = port
self.mqttConnectedCb = mqttConnectedCb
self.mqttDisconnectedCb = mqttDisconnectedCb
self.mqttPublishCb = mqttPublishCb
self.mqttSubackCb = mqttSubackCb
self.Open()

def __str__(self):
Domoticz.Debug("MqttClient::__str__")
if (self.mqttConn != None):
return str(self.mqttConn)
else:
return "None"

def Open(self):
Domoticz.Debug("MqttClient::Open")
if (self.mqttConn != None):
self.Close()
self.isConnected = False

Protocol = "MQTT"
if (self.Port == "8883"):
Protocol = "MQTTS"

self.mqttConn = Domoticz.Connection(Name=self.Address, Transport="TCP/IP", Protocol=Protocol, Address=self.Address, Port=self.Port)
self.mqttConn.Connect()

def Connect(self):
Domoticz.Debug("MqttClient::Connect")
if (self.mqttConn == None):
self.Open()
else:
ID = 'Domoticz_'+str(int(time.time()))
Domoticz.Debug("MQTT CONNECT ID: '" + ID + "'")
self.mqttConn.Send({'Verb': 'CONNECT', 'ID': ID})

def Ping(self):
Domoticz.Debug("MqttClient::Ping")
if (self.mqttConn == None or not self.isConnected):
self.Open()
else:
self.mqttConn.Send({'Verb': 'PING'})

def Publish(self, topic, payload, retain = 0):
Domoticz.Debug("MqttClient::Publish " + topic + " (" + payload + ")")
if (self.mqttConn == None or not self.isConnected):
self.Open()
else:
self.mqttConn.Send({'Verb': 'PUBLISH', 'Topic': topic, 'Payload': bytearray(payload, 'utf-8'), 'Retain': retain})

def Subscribe(self, topics):
Domoticz.Debug("MqttClient::Subscribe")
subscriptionlist = []
for topic in topics:
subscriptionlist.append({'Topic':topic, 'QoS':0})
if (self.mqttConn == None or not self.isConnected):
self.Open()
else:
self.mqttConn.Send({'Verb': 'SUBSCRIBE', 'Topics': subscriptionlist})

def Close(self):
Domoticz.Debug("MqttClient::Close")
#TODO: Disconnect from server
self.mqttConn = None
self.isConnected = False

def onConnect(self, Connection, Status, Description):
if (Status == 0):
Domoticz.Debug("MQTT connected !!!!successfully.")
self.Connect()
else:
Domoticz.Log("Failed to connect to: " + Connection.Address + ":" + Connection.Port + ", Description: " + Description)

def onDisconnect(self, Connection):
Domoticz.Debug("MqttClient::onDisonnect Disconnected from: " + Connection.Address+":" + Connection.Port)
self.Close()
# TODO: Reconnect?
if self.mqttDisconnectedCb != None:
self.mqttDisconnectedCb()

def onMessage(self, Connection, Data):
topic = ''
if 'Topic' in Data:
topic = Data['Topic']
payloadStr = ''
if 'Payload' in Data:
payloadStr = Data['Payload'].decode('utf8','replace')
payloadStr = str(payloadStr.encode('unicode_escape'))

if Data['Verb'] == "CONNACK":
self.isConnected = True
if self.mqttConnectedCb != None:
self.mqttConnectedCb()

if Data['Verb'] == "SUBACK":
if self.mqttSubackCb != None:
self.mqttSubackCb()

if Data['Verb'] == "PUBLISH":
if self.mqttPublishCb != None:
rawmessage = Data['Payload'].decode('utf8')
message = ""

try:
message = json.loads(rawmessage)
except ValueError:
message = rawmessage

self.mqttPublishCb(topic, message)
Loading

0 comments on commit 938c9cf

Please sign in to comment.