Skip to content

Commit

Permalink
Merge pull request #29 from philippeportesppo/service_manager_upnp
Browse files Browse the repository at this point in the history
Service manager upnp
  • Loading branch information
philippeportesppo authored Feb 12, 2018
2 parents 938ce65 + 1681e88 commit 128aab5
Show file tree
Hide file tree
Showing 12 changed files with 1,044 additions and 50 deletions.
File renamed without changes.
2 changes: 1 addition & 1 deletion iaq-vent.groovy → Obsolete/iaq-vent.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ def handleLevel(evt) {
thermostat.each {it.setThermostatFanMode("auto");}

}
}
}
1 change: 1 addition & 0 deletions Obsolete/readme
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This folder contains the smartapp and devicehandlers before I used the GitHubIntegration.
69 changes: 44 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
# AirMentorPro2_SmartThings

<img src="https://github.com/philippeportesppo/AirMentorPro2_SmartThings/blob/master/overallsetup.png" alt="Overall Setup Icon" style="width:50%;height:50%;">

What you need:

Raspberry PI 3 with Apache2 and PHP5 installed properly<p></p>
Raspberry Pi PHP and Apache installation instructions (https://www.raspberrypi.org/documentation/remote-access/web-server/apache.md)
Assign a static IP address to your raspberry on your local network. This project works only if your Hub and raspberry are on same network(otherwise the HubAction won't work and you need to implement external HTTPrequest instead)<p></p>
Optional: 1 USB dongle BT-LE (Plugable Dual-Mode BT-LE/BT model USB-BT4LE) I didn't make it with the internal BT-LE of the Pi using the oringinal image on it, I used this external one then. Then recently, the Raspberry Pi internal BTLE works fine using Linux raspberrypi 4.4.50-v7+ #970 SMP Mon Feb 20 19:18:29 GMT 2017 armv7l GNU/Linux< image from the Raspberry website <p></p>
Additional installation on Raspberry:<p></p>
<li>Bluez (http://www.elinux.org/RPi_Bluetooth_LE)<p></p></li>
<li>BluePy (https://github.com/IanHarvey/bluepy)<p></p></li>
<li>requests (http://raspberrypi-aa.github.io/session4/requests.html)<p></p></li>
Put in Raspberry /var/www/html folder the file : airmentorpro2.php<p></p>
Put in /home/pi/Documents the python script airmentorpro2.py<p></p>
You will launch this first python script by: <b>sudo /usr/bin/python /var/www/html/airmentorpro2.py [your AirMentor MAC] [your hci#] & </b><p></p>Example: sudo /usr/bin/python /var/www/html/airmentorpro2.py fe:ed:fa:ce:be:ef 0 & <p></p>As this script runs an infinit loop, better to fork it with &<p></p>
Put in /home/pi/Documents the python script undergroundweather.py<p></p>
This requires you to get a Weather UnderGround API key from https://www.wunderground.com/weather/api/<p></p>The information is used to provide more data about outside conditions. If you don't want to use this, check the previous versions of the DTH and html page on this GitHub.<p></p>
<p></p> You will launche this script by: <b>sudo /usr/bin/python /var/www/html/undergroundweather.py [yourAPI key] [state] [city] &</b>
<p>As this script runs an infinit loop, better to fork it with & too</p>
In Smartthings IDE: Create a Device Handler (then save and publish for yourself) from air-mentor-pro-2.groovy <p></p>
In Smartthings IDE: Create a SmartApp (then save and publish for yourself) from SmartApp.groovy. The Smartapp is here to allow the alerting on pollution level and polluant. This is an optional app, if you don't want to be notified, you can ignore it.<p>
In Smartthings IDE: Create a SmartApp (then save and publish for yourself) from iaq-vent.groovy. The Smartapp is here to allow piloting vents/swtiches based on selected levels of IAQ</p>
Create a device in Smartthings web page based on this device handler. Put anything as Device Network Id as the Device Handler will overwrite it at first run. Don't ever change it after if your raspberry doesn't change its static IP address otherwise, the parse method is sent for some reason to the former device despite the HubAction is sent by the new instance...<p></p>
Configure the Smarthing device with the IP, port of the Raspberry and URL of the webpage and self-refreshing regularly.You can also access the web page directly by a http://[yourraspberry IP]/airmentorpro2.php?Action=get<p></p>
<b>IMPORTANT:</b> use pollster smartapp to cadence the polling (every 5min) otherwise, Smartthing known issue will let the DTH stoping the polling after 24h or so.<p></p>
<h2>Raspberry Pi 3 side:

<h3>1. Apache2 and PHP5 properly installed <a href="https://www.raspberrypi.org/documentation/remote-access/web-server/apache.md"> (see here) </a> <p>
Assign a static IP address to your raspberry on your local network. This project works only if your Hub and raspberry are on same network (otherwise the HubAction won't work and you need to implement external HTTPrequest instead)<p></p>
Optional: 1 USB dongle BT-LE (Plugable Dual-Mode BT-LE/BT model USB-BT4LE) I didn't make it with the internal BT-LE of the Pi using the oringinal image on it, I used this external one then. Then recently, the Raspberry Pi internal BTLE works fine using Linux raspberrypi 4.4.50-v7+ #970 SMP Mon Feb 20 19:18:29 GMT 2017 armv7l GNU/Linux< image from the Raspberry website (or above)<p></p>
<h3>2. Additional installation on Raspberry:<p>
<h4>Bluez (http://www.elinux.org/RPi_Bluetooth_LE)<p></p>
<h4>BluePy (https://github.com/IanHarvey/bluepy)<p></p>
<h4>>requests (http://raspberrypi-aa.github.io/session4/requests.html)</li><p></p><p>
<h3>3. This project files installation:<p>
<li>Put in Raspberry /var/www/html folder the file : airmentorpro2.php</li>
<li>Put in /home/pi the python script airmentorpro2.py</li>
<li>Put in /home/pi the python script ssdp_server.py (this file uses wlan0 as interface. You can change the code to use eth0 or other)</li>
<li>Put in /home/pi the python script undergroundweather.py</li>
This requires you to get a Weather UnderGround API key from https://www.wunderground.com/weather/api/<p></p>The information is used to provide more data about outside conditions. If you don't want to use this, check the previous versions of the DTH and html page on this GitHub.
<li>Create a folder "lib" in /home/pi</li>
<li>Put in /home/pi/lib the 2 files ssdp.py and upnp_http_server.py.</li>
This is mandatory the 2 files are in a lib folder and the lib folder at the same location as ssdp_server.py
<li>In <b>/etc/rc.local</b>, just before the exit 0 (last line):</li>
<li>add: <b>sudo /usr/bin/python /home/pi/airmentorpro2.py [your AirMentor MAC] [your hci#] & </b><p></p>Example: sudo /usr/bin/python /home/pi/airmentorpro2.py fe:ed:fa:ce:be:ef 0 & <p></p></li>

<li>add: <b>/usr/bin/python /home/pi/ssdp.py &</b></li>
<li>add <b>sudo /usr/bin/python /var/www/html/undergroundweather.py [yourAPI key] [state] [city] &</b></li>

<h2>Smarthings IDE side:<p>

<li> with a github enabled SmartThings IDE (see <a href=http://docs.smartthings.com/en/latest/tools-and-ide/github-integration.html > here</a>), import the namespace philippeportesppo and repository AirMentorPro2_SmartThings on master branch</li> and the following device handlers and smartapps:
<h3>SmartApps:
<li>AirMentor Pro UPnP Service Manager : the smartapp managing the ssdp discover.</li>
<li>IAQ_vent : the smartapp managing vents and AC fans upon air quality notification</li>
<li>Notify Me When for AirMentor Pro : the smartapp managing events and notify you about the air quality.</li>
<h3>Device Handler:
<li>Air Mentor Pro 2 : the device handler to access the AirMentor Pro 2</li>
<p>
If you cannot access github integration, you might have to create the devicehandler and smartapps manually from the code.

<h2>SmartThing Mobile app:<p>
<li>Go to Smartapps section and add the <b>AirMentor Pro UPnP Service Manager</b> smartapp from "My Apps"</li>
<li>Start the research, few seconds later the pi will be discovered, select it, press next and save.</li>
<li>AirMentor Pro 2 will be added


<h3><b>IMPORTANT:</b></h3> use pollster smartapp to cadence the polling (every 5min) otherwise, Smartthing known issue will let the DTH stoping the polling after 24h or so.<p></p>
Hope you like it.

Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ metadata {

preferences {
section {
input "internal_ip", "text", title: "Internal IP", required: true
input "internal_port", "text", title: "Internal Port (80)", required: true
input "internal_query_path", "text", title: "Internal query Path (/airmentorpro2.php?Action=get)", required: true
//input "internal_ip", "text", title: "Internal IP", required: true
//input "internal_port", "text", title: "Internal Port (80)", required: true
//input "internal_query_path", "text", title: "Internal query Path", defaultValue: "/airmentorpro2.php?Action=get", required: true
}
}

Expand Down Expand Up @@ -216,6 +216,20 @@ tiles(scale: 2) {

def installed() {
log.debug "Executing 'installed'"
log.debug getDataValue("ip")
log.debug getDataValue("port")

state.IAQ_event = ""
state.CO2_event = ""
state.PM25_event= ""
state.PM10_event= ""
state.TVOC_event= ""
state.requestCounter = 0

log.debug "state events initialized..."

refresh()


}

Expand All @@ -227,14 +241,7 @@ def updated() {
}

def initialize() {
state.IAQ_event = ""
state.CO2_event = ""
state.PM25_event= ""
state.PM10_event= ""
state.TVOC_event= ""
state.requestCounter = 0

log.debug "state events initialized..."

}

Expand Down Expand Up @@ -375,11 +382,6 @@ def parse(description) {
events << createEvent(name: "UGWtemperaturecallevel", value: convertTemperature(UGW_Temp_float.toFloat(),temperatureScale), unit: temperatureScale)
events << createEvent(name: "UGW_Icon_UrlIcon", value: UGW_Icon_Nt.toString()+UGW_Icon_Url.toString())



state.refreshCounter = state.refreshCounter + 1
// log.debug state.refreshCounter

log.debug "Generating alerts if not good"

def map = generate_app_event( "IAQ",iaq_int.toInteger(), state.IAQ_event, 50, 100,150, 200)
Expand Down Expand Up @@ -497,17 +499,23 @@ private String convertPortToHex(port) {
def refresh() {
log.debug "Executing refresh"

def host = internal_ip
def port = internal_port
def hosthex = convertIPtoHex(host)
def porthex = convertPortToHex(port)
//log.debug "The device id before update is: $device.deviceNetworkId"
device.deviceNetworkId = "$hosthex:$porthex"
def host = getDataValue("ip")//internal_ip
log.debug "Executing refresh 2"

def port = getDataValue("port")//internal_port
log.debug "Executing refresh 3"

//def hosthex = convertIPtoHex(host)
log.debug "Executing refresh 4"

//def porthex = convertPortToHex(port)
log.debug "The device id before update is: $device.deviceNetworkId"
device.deviceNetworkId = "$host:$port"

//log.debug "The device id configured is: $device.deviceNetworkId"
log.debug "The device id configured is: $device.deviceNetworkId"

def path = internal_query_path
//log.debug "path is: $path"
def path = getDataValue("query_path")
log.debug "path is: $path"

def headers = [:]
headers.put("HOST", "$host:$port")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* Generic UPnP Service Manager
*
* Copyright 2018 Philippe Portes based on SmartThings original UPnP Service Manager SmartApp
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "AirMentor Pro UPnP Service Manager",
namespace: "philippeportesppo",
author: "Philippe Portes",
description: "Discover and configures your Raspberry Pi bridge to AirMentor Pro (see https://github.com/philippeportesppo/AirMentorPro2_SmartThings).",
category: "SmartThings Labs",
iconUrl: "https://raw.githubusercontent.com/philippeportesppo/AirMentorPro2_SmartThings/master/images/app-icon_bw.png",
iconX2Url: "https://raw.githubusercontent.com/philippeportesppo/AirMentorPro2_SmartThings/master/images/app-icon_bw.png",
iconX3Url: "https://raw.githubusercontent.com/philippeportesppo/AirMentorPro2_SmartThings/master/images/app-icon_bw.png")


preferences {
page(name: "searchTargetSelection", title: "UPnP AirMentor via Pi Search Target", nextPage: "deviceDiscovery") {
section("Search Target") {
input "searchTarget", "string", title: "Search Target", defaultValue: "urn:schemas-upnp-org:device:AirMentorPro2:1", required: true
}
}
page(name: "deviceDiscovery", title: "UPnP Device Setup", content: "deviceDiscovery")
}

def deviceDiscovery() {
def options = [:]
def devices = getVerifiedDevices()
devices.each {
def value = it.value.name ?: "AirMentorPro ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
def key = it.value.mac
options["${key}"] = value
}

ssdpSubscribe()

ssdpDiscover()
verifyDevices()

return dynamicPage(name: "deviceDiscovery", title: "Discovery Started!", nextPage: "", refreshInterval: 5, install: true, uninstall: true) {
section("Please wait while we discover your AirMentorPro Device. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedDevices", "enum", required: false, title: "Select Devices (${options.size() ?: 0} found)", multiple: true, options: options
}
}
}

def installed() {
log.debug "Installed with settings: ${settings}"

initialize()
}

def updated() {
log.debug "Updated with settings: ${settings}"

unsubscribe()
initialize()
}

def initialize() {
unsubscribe()
unschedule()

ssdpSubscribe()

if (selectedDevices) {
log.debug "Selected device"
addDevices()
}

runEvery5Minutes("ssdpDiscover")
}

void ssdpDiscover() {
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${searchTarget}", physicalgraph.device.Protocol.LAN))
}

void ssdpSubscribe() {
subscribe(location, "ssdpTerm.${searchTarget}", ssdpHandler)
}

Map verifiedDevices() {
def devices = getVerifiedDevices()
def map = [:]
devices.each {
def value = it.value.name ?: "AirMentorPro ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
def key = it.value.mac
map["${key}"] = value
log.debug map
}
map
}

void verifyDevices() {
def devices = getDevices().findAll { it?.value?.verified != true }
devices.each {
int port = convertHexToInt(it.value.deviceAddress)
String ip = convertHexToIP(it.value.networkAddress)
String host = "${ip}:${port}"
sendHubCommand(new physicalgraph.device.HubAction("""GET ${it.value.ssdpPath} HTTP/1.1\r\nHOST: $host\r\n\r\n""", physicalgraph.device.Protocol.LAN, host, [callback: deviceDescriptionHandler]))
}
}

def getVerifiedDevices() {
getDevices().findAll{ it.value.verified == true }
}

def getDevices() {
if (!state.devices) {
state.devices = [:]
}
state.devices
}

def addDevices() {
def devices = getDevices()

log.debug devices

selectedDevices.each { dni ->
def selectedDevice = devices.find { it.value.mac == dni }
log.debug selectedDevice
def d
if (selectedDevice) {
d = getChildDevices()?.find {
it.deviceNetworkId == selectedDevice.value.mac
}
}

if (!d) {
addChildDevice("philippeportesppo", "Air-Mentor-Pro-2", selectedDevice.value.mac, selectedDevice?.value.hub, [
"label": selectedDevice?.value?.name ?: "AirMentorPro",
"data": [
"mac": selectedDevice.value.mac,
"ip": selectedDevice.value.networkAddress,
"port": "0050",
"query_path":"/airmentorpro2.php?Action=get"
]
])
}
}
}

def ssdpHandler(evt) {
def description = evt.description
def hub = evt?.hubId

def parsedEvent = parseLanMessage(description)
parsedEvent << ["hub":hub]

def devices = getDevices()
String ssdpUSN = parsedEvent.ssdpUSN.toString()

if (devices."${ssdpUSN}") {
def d = devices."${ssdpUSN}"
//log.debug d
//log.debug parsedEvent
if (d.networkAddress != parsedEvent.networkAddress || d.deviceAddress != parsedEvent.deviceAddress) {
d.networkAddress = parsedEvent.networkAddress
d.deviceAddress = parsedEvent.deviceAddress
def child = getChildDevice(parsedEvent.mac)
log.debug "Child: "
if (child) {
log.debug child
child.sync(parsedEvent.networkAddress, parsedEvent.deviceAddress)
}
}
} else {
devices << ["${ssdpUSN}": parsedEvent]
}
}

void deviceDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
def body = hubResponse.xml
def devices = getDevices()
def device = devices.find { it?.key?.contains(body?.device?.UDN?.text()) }
if (device) {
device.value << [name: body?.device?.roomName?.text(), model:body?.device?.modelName?.text(), serialNumber:body?.device?.serialNum?.text(), verified: true]
}
}

private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}

private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
Loading

0 comments on commit 128aab5

Please sign in to comment.