OpenSprinkler Integration

How about some OpenSprinkler integration?

API doccumentation: [https://rayshobby.net/docs/os_fw211_api.pdf]

Figured I'd ask here before I start taking n00b stabs at getting this done.

Desired controls would be something along the lines of system enable/disable, manual zone control for watering/testing(maybe a fixed variable). Using the Voice assistants for things like; Alexa, enable/disable the sprinkler system. or Alexa hose off the solicitors.

I was wondering if you ever got your OpenSprinkler integrated into HE? I'm in the same boat and have been using OpenHab + Homekit to control mine, but it would be great to have it on HE.

@graysonlough Have you had any luck with this?

I have not gotten to it yet.

I have a Node Red flow running on a Raspberry pi that does some of this with Opensprinkler. It is not very sophisticated, but I would be happy to share if anyone wants it.

The integration to OpenSprinkler would be a huge one. Two open companies working together. And, Spring is around the corner. Who do I send donuts to for encouragement? :slight_smile:

I saw this while looking for a Yamaha Receiver integration


1 Like

has any one tried this - about to pull the trigger on buying this

Going strong since 2014, I have the the os pi version with 1 zone expansion board.

@jeff3
thanks for the info -

Do you have a copy of that code that you can share ? seems to be gone from GitHub

/**

  • OpenSprinkler Controller Driver
  • Copyright 2018 Ben Rimmasch
  • The repository will probably be found here:
  • https://github.com/codahq/hubitat_codahq
  • 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.

*/

import groovy.json.JsonSlurper
import java.security.MessageDigest

def getSTATUS() {
return "status"
}

def getENABLED() {
return "enabled"
}

def getSTATION_RUN() {
return "station-run"
}

def getSTATION_OFF() {
return "station-off"
}

def getRUN_ONCE_RUN() {
return "run-once-run"
}

def getRUN_ONCE_OFF() {
return "run-once-off"
}

def getALL() {
return "all"
}

metadata {
definition(name: "OpenSprinkler Controller", namespace: "codahq-hubitat", author: "Ben Rimmasch") {
capability "Refresh"
capability "Sensor"
capability "Configuration"
capability "Switch"
capability "Valve"

//attribute "operationEnabled", "bool"

}

preferences {
input("password", "text", title: "Device Key", description: "Your OpenSprinker device password")
input("ipadd", "text", title: "IP address", description: "The IP address of your OpenSprinkler unit", required: true)
input("port", "text", title: "Port", description: "The port of your OpenSprinkler unit", required: true)
input name: "descriptionTextEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
input name: "traceLogEnable", type: "bool", title: "Enable trace logging", defaultValue: true
input name: "pollingInterval", type: "number", range: 5..3600, title: "Polling Interval", description: "Duration in seconds between polls", defaultValue: 60, required: true
}
}

def installed() {
initialize()
}

def uninstalled() {
getChildDevices().each {
log.warn "Deleting device ${it.label} with DNI ${it.deviceNetworkId}"
deleteChildDevice(it.deviceNetworkId)
}
}

def updated() {
if (!state.initialized) {
initialize()
}
try {
if (ipadd != null && port != null) {
if (device.deviceNetworkId != getHexHostAddress()) {
device.deviceNetworkId = getHexHostAddress()
logInfo "Device Network ID set to: ${device.deviceNetworkId}"
}
}
else {
log.warn "IP and port must be configured in the device's preferences in the IDE."
}
}
catch (Exception e) {
log.warn "Couldn't set Device Network ID: ${e}"
}
if (password != null) {
state.hash = generateMD5(password)
device.updateSetting("password", [type: "STRING", value: ""])
}
if (state.hash == null) {
log.warn "A password must be configured in the device's preferences in the IDE."
}
configure()
refresh()
}

def initialize() {
logDebug "Initialize triggered"

state.initialized = 1

logInfo "Receiving local POST on ${device.hub?.getDataValue('localIP')}:${device.hub?.getDataValue('localSrvPortTCP')}"
}

def configure() {
api(ALL)
}

def refresh() {
logInfo "Refreshing status from ${device.label}"
unschedule()
state.updatedDate = now()
api(STATUS)
api(ENABLED)
customPolling()
}

def open() {
unschedule()
def durations = "["
state.stations.eachWithIndex {
station, idx ->
int dur
if (!station.disabled) {
def child = getChildDevice(getChildDeviceId(idx))
dur = child.duration() == null ? 0 : child.duration()
}
else {
dur = 0
}
durations += "${dur},"
}
durations = durations.substring(0, durations.length() - 1) + "]"
logTrace durations

api(RUN_ONCE_RUN, [durations: durations])
}

def close() {
unschedule()
api(RUN_ONCE_OFF)
}

def customPolling() {
logTrace "customPolling(${pollingInterval}) now:${now()} state.updatedDate:${state.updatedDate}"
if (!isConfigured()) {
logInfo "Polling canceled. Please configure the device!"
return
}
double timesSinceContact = (now() - state.updatedDate).abs() / 1000 //time since last update in seconds
logDebug "Polling started. timesSinceContact: ${timesSinceContact} seconds"
if (timesSinceContact > pollingInterval) {
logDebug "Polling interval exceeded"
refresh()
}
runIn(pollingInterval, customPolling) //time in seconds
}

def api(method, args = []) {
logDebug "api(${method}, ${args})"
def methods = [
"status": [gdipadd: "${ipadd}", gdport: "${port}", gdpath: "/js?pw=${state.hash}", gdtype: "GET"],
"station-run": [gdipadd: "${ipadd}", gdport: "${port}", gdpath: "/cm?pw=${state.hash}&sid=${args.sid}&en=1&t=${ args.duration != null ? args.duration : 30 }", gdtype: "GET"],
"station-off": [gdipadd: "${ipadd}", gdport: "${port}", gdpath: "/cm?pw=${state.hash}&sid=${args.sid}&en=0", gdtype: "GET"],
"run-once-run": [gdipadd: "${ipadd}", gdport: "${port}", gdpath: "/cr?pw=${state.hash}&t=${ args.durations != null ? args.durations : "[0, 0, 0, 0, 0, 0, 0, 0]" }", gdtype: "GET"],
"run-once-off": [gdipadd: "${ipadd}", gdport: "${port}", gdpath: "/cv?pw=${state.hash}&rsn=1", gdtype: "GET"],
"all": [gdipadd: "${ipadd}", gdport: "${port}", gdpath: "/ja?pw=${state.hash}", gdtype: "GET"],
"enabled": [gdipadd: "${ipadd}", gdport: "${port}", gdpath: "/jc?pw=${state.hash}", gdtype: "GET"]
]

if (method == STATION_RUN && device.currentValue("valve") != "open") {
logInfo "A station valve is open!"
sendEvent([name: "valve", value: "open", isStateChange: true])
}

def request = methods.getAt(method)
doRequest(request.gdipadd, request.gdport, request.gdpath, request.gdtype)

//http://10.10.10.250/jp?cr=ec5e317122e24ef7354e94697ef321c0&t=[5,0,5,0,5,0,5,0]

}

private doRequest(gdipadd, gdport, gdpath, gdtype) {
logDebug "doRequest($gdipadd, $gdport, $gdpath, $gdtype)"
if (!isConfigured()) {
logInfo "Request canceled. Please configure the device!"
return
}

def hexHostPort = getHexHostAddress()

logTrace "Hex Host:Port is : ${hexHostPort}"
logTrace "DNI is ${device.deviceNetworkId}"
logTrace "And just for good measture: ${getHostAddress()}"
logTrace "Path is: ${gdpath}"

def headers = [: ]
headers.put("HOST", "${gdipadd}:${gdport}")

try {
logTrace "About to create HubAction"
def hubAction = new hubitat.device.HubAction(
[
method: gdtype,
path: gdpath,
headers: headers
]
, "${hexHostPort}"
)
logTrace "After HubAction: ${hubAction}"
return sendHubCommand(hubAction)
}
catch (Exception e)
{
logDebug "Hit exception in doRequest: ${hubAction}"
logDebug e
}
}

def parse(description) {
logDebug "start parse"

try {
def msg = parseLanMessage(description)
logTrace "msg: ${msg}"

def slurper = new groovy.json.JsonSlurper()
def json = slurper.parseText(msg.body)

logTrace json

if (json.settings) {
  handleSetup(json)
}
if (json.sn) {
  handleStationStatus(json)
}
if (json.en) {
  handleEnabled(json.en)
}
if (json.result || json.refresh) {
  if (json.result != 1) {
    log.warn "Last action was not successful! Result: ${json.result}"
  }
  logInfo "Update needed.  Doing refresh!"
  runIn(3, refresh, [overwrite: false])
}

}
catch (Exception e)
{
logDebug "Hit exception in parse"
logDebug e
}
}

private handleSetup(json) {
logDebug "handleSetup() ${json}"
def stations = handleStationNames(json.stations)
stations.eachWithIndex {
station, idx ->
if (idx < state.nstations && !station.disabled) {
logDebug "Checking enabled station ${station} at index ${idx}"

  def deviceId = getChildDeviceId(idx)
  if (!getChildDevice(deviceId)) {
    addChildDevice("codahq-hubitat", "OpenSprinkler Station", deviceId, [name: "OpenSprinkler Station", label: "OS Station ${station.name}", isComponent: false])
    logInfo "Added station ${station.name} with device id ${deviceId}"
  }

}
else {
  def child = getChildDevice(getChildDeviceId(idx))
  if (child) {
    log.warn "Station ${child.label} is no longer enabled and will not be used."
  }
}

}
handleEnabled(json.settings.en)
}

private handleStationStatus(json) {
logDebug "handleStationStatus() ${json}"
if (json.nstations && json.nstations != state.nstations) {
state.nstations = json.nstations
logInfo "Number of stations set to ${state.nstations}"
}
if (!json.sn) return
def stations = []
def valveOpen = false
state.stations.eachWithIndex {
station, idx ->
logTrace "${station} index:${idx} open:${json.sn[idx]}"
//if (json.sn[idx] == 1) {
// logInfo "Station ${station.name} (${idx}) is open"
//}

def child = getChildDevice(getChildDeviceId(idx))
if (child != null) {
  logTrace "child.state.switch: ${child.currentValue("switch")}"
  def switchState = station.disabled == 1 ? "off" : "on"
  if (child.currentValue("switch") != switchState) {
    child.sendEvent([name: "switch", value: switchState, isStateChange: true])
    logInfo "Station ${station.name} is ${switchState}"
  }

  logTrace "child.state.valve: ${child.currentValue("valve")}"
  def valveState = json.sn[idx] == 1 ? "open" : "closed"
  if (child.currentValue("valve") != valveState) {
    child.sendEvent([name: "valve", value: valveState, isStateChange: true])
    logInfo "Station ${station.name} is ${valveState}"
  }
}

stations << [name: station.name, open: json.sn[idx], disabled: station.disabled]
valveOpen = valveOpen || json.sn[idx] == 1

}

def valveState = valveOpen ? "open" : "closed"
if (device.currentValue("valve") != valveState) {
logInfo "OpenSprinker is ${valveState}"
sendEvent([name: "valve", value: valveState, isStateChange: true])
}

state.stations = stations
return stations
}

private handleStationNames(json) {
logDebug "handleStationNames() ${json}"
if (!state.nstations) return
if (!json.stn_dis) return

def bits = convertIntToBitSet(json.stn_dis[0])
logDebug bits

boolean[] disabled = new boolean[state.nstations]

int index = 0;
for (int i = bits.length() - 1; i >= 0; i--)
{
disabled[i] = bits.charAt(index) == "1"
index++
}

def stations = []
json.snames.eachWithIndex {
name, idx ->
if (idx < state.nstations) {
logDebug "Found station ${name} at index ${idx}"
stations << [name: name, disabled: disabled[idx]]
}
}
state.stations = stations
}

private handleEnabled(enabled) {
logDebug "handleEnabled($enabled)"
def value = enabled ? "on" : "off"
if (device.currentValue("switch") != value) {
logInfo "OpenSprinkler Controller is ${value}"
sendEvent([name: "switch", value: value, displayed: true, isStateChange: true])
}
}

/General Helper Methods/
private isConfigured() {
return ipadd && port && state.hash
}

/To Hex Helper Methods/
private String convertIPToHex(ipAddress) {
String hex = ipAddress.tokenize('.').collect { String.format('%02x', it.toInteger()) }.join()
logTrace "IP address entered is ${ipAddress} and the converted hex code is ${hex}"
return hex.toUpperCase()
}
private String convertPortToHex(port) {
String hexport = port.toString().format('%04x', port.toInteger())
logTrace "Port entered is ${port} and the converted hex port is ${hexport}"
return hexport.toUpperCase()
}

/Out of Hex Help Methods/
//private Integer convertHexToInt(hex) {
// if (isDebug()) log.debug "Convert hex to int: ${hex}"
// return Integer.parseInt(hex,16)
//}
//private String convertHexToIP(hex) {
// if (isDebug()) log.debug("Convert hex to ip: $hex") // a0 00 01 6
// [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
//}
//private getHostAddress() {
// def parts = device.deviceNetworkId.split(":")
// if (isDebug()) log.debug "Device Network ID: $device.deviceNetworkId"
// def ip = convertHexToIP(parts[0])
// def port = convertHexToInt(parts[1])
// return ip + ":" + port
//}
private getHostAddress() {
return "${ipadd}:${port}"
}

private getHexHostAddress() {
def hosthex = convertIPToHex(ipadd)
def porthex = convertPortToHex(port)
if (porthex.length() < 4) {
porthex = "00" + porthex
}
logTrace "Hosthex is : $hosthex"
logTrace "Port in Hex is $porthex"
return "${hosthex}:${porthex}"
}

def generateMD5(String s){
MessageDigest.getInstance("MD5").digest(s.bytes).encodeHex().toString()
}

private convertIntToBitSet(int bits) {
logTrace bits
Integer.toBinaryString(bits)
}

private getChildDeviceId(index) {
return "${device.deviceNetworkId}-${index}".toString()
}

private logInfo(msg) {
if (descriptionTextEnable) log.info msg
}

def logDebug(msg) {
if (logEnable) log.debug msg
}

def logTrace(msg) {
if (traceLogEnable) log.trace msg
}

/**

  • OpenSprinkler Station Driver
  • Copyright 2018 Ben Rimmasch
  • 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.

*/

metadata {
definition(name: "OpenSprinkler Station", namespace: "codahq-hubitat", author: "Ben Rimmasch") {
capability "Refresh"
capability "Valve"
capability "Actuator"
capability "Switch"
capability "Sensor"

command "open", [[name: "Delay Open", type: "NUMBER", description: "Enter a value in seconds to delay before opening"], [name: "Duration", type: "NUMBER", description: "Enter a value in seconds to run before closing"]]

}

preferences {
input name: "descriptionTextEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: false
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
input name: "traceLogEnable", type: "bool", title: "Enable trace logging", defaultValue: false
input name: "defaultRuntime", type: "number", range: 1..64800, title: "Default Duration", description: "Default duration in seconds this station will run if no duration is specified", defaultValue: 60, required: true
input name: "runOnceRuntime", type: "number", range: 1..64800, title: "Run-once Duration", description: "Duration in seconds this station will run when the system runs a run-once program", defaultValue: 60, required: true
}
}

def installed() {
initialize()
}

def updated() {
initialize()
}

def initialize() {
sendEvent(name: "switch", value: "on")
sendEvent(name: "valve", value: "closed")
}

def refresh() {
parent.refresh()
}

def open(BigDecimal delay = null, BigDecimal duration = null) {
logDebug "open(${delay},${duration})"
unschedule()
if (delay != null) {
logInfo "Opening ${device.label} in ${delay} seconds"
runIn(delay.toInteger(), callbackOpen, [overwrite: false, data: [duration: duration]])
return
}
else {
logTrace "duration ${duration}"
sendEvent([name: "valve", value: "open", isStateChange: true])
def runTime = duration == null ? defaultRuntime.toInteger() : duration.toInteger()
logInfo "Opening station ${device.label} for ${runTime} seconds"
runIn(runTime + 5, refresh, [overwrite: false])

parent.api(parent.getSTATION_RUN(), [sid: stationIndex, duration: runTime])

}
}

def callbackOpen(data) {
logDebug "callbackOpen($data)"
open(null, data.duration)
}

def close() {
logDebug "close()"
unschedule()
logInfo "Closing station ${device.label}"
sendEvent([name: "valve", value: "closed", isStateChange: true])
parent.api(parent.getSTATION_OFF(), [sid: stationIndex])
}

def duration() {
logTrace "duration() ${runOnceRuntime}"
return runOnceRuntime as Integer
}

def off() {
//logDebug "off()"
//unschedule()
//close()
//not implemented
}

def on() {
//logDebug "on()"
//not implemented
}

private getStationIndex() {
return new String(device.deviceNetworkId).tokenize('-')[1]
}

private logInfo(msg) {
if (descriptionTextEnable) log.info msg
}

def logDebug(msg) {
if (logEnable) log.debug msg
}

def logTrace(msg) {
if (traceLogEnable) log.trace msg
}

I was curious if anything had evolved with this project. I just picked up an open sprinkler module. It's unclear from this discussion what this integration offers compared to the app Opensprinkler has. I don't see anything on GitHub so wondering if this was abandoned.

I just used the code that @graysonlough posted in 6/20.

Worked great right out of the box!

I have 3 hubs and created an "OpenSprinkler" controller for each. When I configured the device, the driver interrogated the hardware and created a child device for each station.

You can open and close valve on the child devices with optional delay and duration parameters.

I guess Ben Rimmasch wrote the driver. Thanks to Ben and graysonlough!

Can you interact with the app they provide, or only use it for direct control of the valves? Their app seems to have some limitations in how it uses weather info and sensors - it would be great to take advantage of other information and still use the open sprinkler scheduling.

Yes, I use the open sprinkler app on my Android phone--love it.

This looks great, but I am still trying to install. Can you help a fellow hubitater? It has been over a year since I even looked at the hubitat since it has ran at least 18 months without a problem : ) As such, I have lost some familiarity.

I see graysnlough made two code posts. One code is 454 lines long, the other is 118 lines long. Both include "driver" in the header so I suspect they are drivers. I am a career computerthingen person, but don't work in .json nor hubitat natively.

As such, I tried to past both versions of code in both the 'user apps' and the 'user drivers' section of hubitat. The 'user app' section refuses both pieces of graysonlough's code. However the 'user drivers' section accepts both versions of graysonlough's code. I tried to get both to work, and both seem to do the same thing (not much so far).

I found after pasting his code, I could add a virtual OpenSprinkler device, then configure IP address and port as shown below. My OpenSprinkler does not have a password so I left that field blank.

I hit the configure a few tens of times, but never get anything. The 'events' window remains blank.

I then tried to write a rule to control the OpenSprinkler device, but it does not show any individual valves. I am seemingly missing something critical. Also, the IP address set in hubitat for the OS device is pingable and accessible via the port number I entered. I doubt it matters the hubitat and OS are on separate vlans and I temporarily dropped all ACLs between the two associated vlans. Traffic should flow.

What else did anybody do to get it to work?
Many thanks if you can help get my sprinklers integrated, I have wanted to get this working for a long time.

You need to set your port to 8080.

Many thanks for the reply. I added/removed both drivers and both devices several times and figure the way to interact with it is via rule machine. When trying to create a rule to control a valve, both the OpenSprinkler Controller device and the OpenSprinkler Station device show up as a single checkbox, but I never saw a way to choose which of 8 valves to interact with, just the two devices themselves. I tried every combination of adding/removing the devices and rebooting HE before hitting the 'configure' button in he OpenSprinkler Controller page I could imagine so my failure is at least not due to a lack of spanning permutation space.

Via HE logging I can see the controller is talking to my OpenSprinkler device without errors, and when this is the case the controller device sees my OS has 8 valves as shown below.

Since I never found a way to interact with the individual valves, I gave up. If anybody else is trying to get this working: a lot of my wasted time came down to finding communication would not work with the OS host because I have no password set in the OS host and left the password field blank in the OpenSprinkler Controller driver. Once I added a password to the OS host and the the Controller driver and found/unclicked the 'ignore password' checkbox in the OS host, communication began working. At least I think that is what happened while I spent a half day 'exploring' it. Routing the traffic between vlans is apparently not a problem as far as I can tell.

The github repo seems to be down. Are you ale to re-copy this code into a new message on this forum within code brackets so it formats correctly?