?

Log in

No account? Create an account

The Toybox

people for the conservation of limited amounts of indignation


Previous Entry Share Next Entry
i am having a crisis of coding
children of dune - leto 1
seperis
I am currently debating whether I can pack and take my home server to Escapade. I'll talk myself into sanity soon, don't worry. Hopefully.

Home Automation

Anyone else here use SmartThings for home automation? I just finished coding my Bathroom Automation SmartApp in the IDE. It seems to be working--benefit of being a professional QA/QC/Program Tester is that you learn how to test things properly--but half of SmartThings users who customize moved over to WebCore, which I'd need at least a long weekend to sit down with and learn. So there is a lot less code scraps now that are less than two years old.

I like hand coding, though; it's soothing. But then there's this weirdness; I finished Bathroom Automation, which uses a motion sensor, a water sensor, and three lights, in like two days. This isn't my first try, though: a year ago, I tried and completely failed to make it work or even understand it.



Generally, I can read any code; give me a basic program--NOT THE CODE OUT OF CONTEXT--and an hour, and even if it's a new language, I can read it, and its only a hop to editing and a skip to writing my own. That's probably true of most coders; once you learn one, pretty much all of them are just variations on that. Variables, functions, objects: they're pretty much ubiquitous, and the bells and whistles are different, but the idea is the same.

In fact, the biggest difference I've found overall is variables; that seems to be the place where the variation goes from 'define the fuck out of the type right down to how long the number is and whether a decimal may or may not be involved and do it twice' to "don't even fucking care could be 'cat' or '4.5e3'". It baffled me, but I went with it for the most part because following the rules was how one get a program to run.

Then came VBA, which was what finally did the trick, but it happened in stages over years.

When I first learned VBA, I did it the old fashioned way: recorded macro and after random macro doing differnet shit and reading the code. When I was comfortable editing and combining, I started creating my own subs and then opened VBA reference and came to a shocked stop: the baffling concepts of Dims and Sets in variables.

Dims and sets: dims to declare a variable type, and sets to create a variable value.

Example from Excel:
Dim wks As Worksheet
Dim ListTable As Range
Set wks = Worksheets("MyWorksheet")
Set ListTable = Range("MyNamedRange")


So I dutifully learned it--sure, most of my early scripts didn't deal with this bullshit, but I was going into way more complicated stuff. And half the goddamn time, even doing what it told me, I'd run it and get a goddamn variable error on those 'sets' and sometimes on the Dims. To get my worksheet in a workbook I dimmed a 'worksheet' but set a 'worksheets'. Dimming a 'worksheets' did not work.

It was not a happy time in my life, lets put it that way.

However, I did find one thing that seemed to work. I didn't dim anything but the worksheet and ranges: no integers, no longs, no doubles, no strings, nothing else. The only thing I set was the worksheet. Then I ran it and only added a dim/set to those that gave me errors. The moment it worked, I declared it done and moved the fuck on.

This worked super well, so well I just copy/pasted and ignored the problem and in fact ignore even asking the question 'why' because while I knew the answer wasn't 'because bullshit', I just didn't care. My programs became a couple of magnitudes more complex, but that stayed the same; I had sometimes ten or twenty of these in a single workbook and as long as it ran, I called it good.

Then came my Down to Agincourt spreadsheets, and slowly--very slowly--I realized there was a problem.

One of the subs was a word counter I pulled from somewhere; my worksheet would access Word--either while the Word doc was open or open it for me--and count the words. It was fairly simple; I had to add some extra libraries to do the link up but after that, it was fairly straightforward. Why did I bother when Word has a perfectly workable counter?

I needed it was because I needed some bells and whistles: specifically, I needed it to do word counts between consecutive bookmarks (aka Day_1, Day_32) because the etnire story organization was based on Days and if--IF--I ever decided to post it, I'd need chapters. It was a way to pass the time, is what I"m saying. It would also let me see word count on POV; all but one or two of those sections between bookmarks had only one pov. So after a lot of googling and trial and error, I pulled the entire bookmark list from Word in location order and figured out how to count between sections with ranges and a lot of looping. So it would access the Word document once to get the bookmarks, and then with pairs of bookmarks until there were no more bookmarks. At this point, being me, I also added some special functions so I could see a word count change, so moving values around.

It took some time, sure, but it wasn't too bad. Then again, at the time, the document was roughly 100K to 200K words. By 500K, I would make a snack and worry about timeouts until it was finished running.

So I did my first breakdown of Agincourt by book and added a lastbookmark delimiter for each book; only count words between bookmarks starting at Bookmark_X and stop when you reach Bookmark Y:fine. That helped, but not all that much, and I found that baffling as fuck.

Next try: refactoring and separation. There was a lot of redundancy in some of the code, one, and two, a lot of looping that could easily be consolidated. By this point, I'd also learned more VBA, so I created some functions whose only job was to loop shit, subbed a few things, and cut my lines of code in half. Sped up, but not enough. Even the first book at about 150K was still slower than it'd been when I was doing the whole document at 300K.

Now, short digression: the Agincourt workbook houses the entire timeline, character list(s), team changes and when, history, demographics, everything I would need for references. The word counter was the biggest sub with the most movable parts, but there were ones I wrote for filtering, updating, dating correctly, everything I needed to use for reference, from who is team leader when and who was on their team, when that changed, deaths, to who was involved with who and when. I documented everything, because sure, the reader may not notice I said Sanjay's first language was Hindi but accidentally switched to Telugu in Book Five, I'd notice and be haunted by it forever. When I say demographics, I mean, everything down to country of origin and city of birth.

(Unexpected danger that should have been obvious: I learned very fast when picking names, if I could google the most common names by city or region by year of birth instead of country + year, I did. That way, I didn't end up giving a region-specific common name to someone from the other side of the country who spoke an entirely different language or dialect. Having so many Indian coworkers helped me get familiar with names, but I still live in terror that one of my characters is actually the Indian equivalent of Ebony Darkness or something.)

Why is this important enough for a digression? There are eight modules of functions. A lot of those functions are volatile; a lot of the subs I used accessed functions that were volatile. Volatile means they are always checking, every second. They are in fact checking constantly while my word count sub was running. In this time of growing word count, sure, that was a factor, but it might also be that my sub never ran alone; it ran with about a dozen volatile functions that were also running at the same time. But I'm getting ahead of the story: I only figured that out later.

This is where I discovered why I needed dims and sets; when dims declared type, they also reserved the exact amount of space required for that type. Set gave it the exact amount. So in an ideal program, it would start already aware of the minimum and maximum amount of space needed; the difference between min and max would be those certain variables that you could not or should not set.

I resisted this for a while: it wouldn't be just refactoring, but actually going through every line of code in eight modules, adding dims and sets to everything, then once I got the error on variables, finding out if it was the dim or the set or both, and only when everything else was eliminated, I could remove the set and leave it with a dim. But everything had to have a dim: everything.

That meant I had to write test scripts: subs whose only function was to hold dummy values and access other subs and functions so I could watch them run, then run them with actual values from the spreadsheet. Only when it passed both was it good to go.

(The ruleset for whether something should be 'set' or not is--not easy. Even after extensive googling, the best I could decide as hard and fast is that any variable acting as reference and therefore not subject to change should be set to value if at all possible.)

This cut my time considerably, but also introduced me to what my volatiles were doing; when I ran a scirpt manually step by step, I watched it jump a dozen times to a separate volatile that was checking date or whatever and thought, okay, then. So I removed Application.Volatile from all functions that didn't absolutely require it, switched others to subs to do manually, and only after that nightmare of rewriting, was googling randomly and ran across a messageboard that said "hey, to speed up your subs, turn off all your functions, it's easy! Here's how to do it:"

Application.Calculation=xlManual

Then before the very last line to turn them all back on:

Application.Calculation=xlAutomatic

It runs fast now. So does my Pokemon spreadsheet which is a terrifying story of multiple super-subs and the stuff of nightmares.



Okay, that's super interesting, you say (if you actually read all that), but what does that have to do with SmartThings? Did you get distracted? Yes, but also, variables.

SmartThings uses Groovy. You don't declare your variables--no wait, you do. Because everything is a fucking variable.



Almost everything: I'll get to that.

A SmartApp is basically a quick way to set up advanced if-thens: "If motion is detected Here, then do This". There are many public apps created by SmartThings or users that SmartThings accepted for public use, but you can also create your own for yourself, publish them in your SmartThings IDE, and use yourself.

There are three basic parts to a SmartApp: Definition, Preferences, and your commands. I'm going to use a simplifed version of my Closet Light app.

Definition is the name ,the creator, category of app, icon, that stuff.

Example:

definition(
    name: "Closet Door Light",
    namespace: "seperis",
    author: "Seperis",
    description: "Turn your lights on when a open/close sensor opens.",
    category: "My Apps",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance@2x.png"
)


Preferences is a little misleading; this is where you tell the app what device types are going to be used by this app. This is also where the user selects what specific devices within these device types will be used. So its both user interface and where you declare your variables and set them.


preferences {
    section("When this door opens:") {
	input "contact1", "capability.contactSensor", title: "Select door"
    }
    section("Turn on this light:") {
	input "switch1", "capability.switch", title: "Select light"
    }
}


Easy right?

Section ("text") are what the user sees in the app and contain instructions for the user: select one of these. When the user clicks in the app, they see a list of devices to select from.

The second line is the important part.

It contains the variable that will hold what the user selects ("contact1"). The second part is to define the kind of device will appear in that drop down list. "capability.contactSensor" means all devices the user has that have the capability to open/close. Title is the text the user will see just above the drop down. You don't have to put that if that's the only input in that section.

Now, you're wondering about the def thing. That's the third section: all the functions and commands. AND THEY'RE ALL FUCKING VARIABLES.


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

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

def initialize()
{
    log.debug "Initializing"
    subscribe(contact1, "contact.open", contactOpenHandler)
    subscribe(switch1, "switch", switchHandler)
}

def contactOpenHandler(evt) {
    log.debug "$evt.name: $evt.value"
    log.debug "turning on lights"
    switch1.on()
}

def switchHandler(evt){
	log.debug "$evt.name: $evt.value"
}


In order: the first two definite the install and change parameters; both refer to intialize, where you define what you're going to be doign with them.

Subscribe is a fancy way to access the capabilities. Within you see the variable we defined in preferences, then the important part: we know it's a contact sensor, but contact sensor has a lot of things it can do. The string isn't random; it's a reference to what specific event within the capability.contactSensor you want to use; here, I subscribed to the event "open", so "contact.open". The third thing is super important; that is the function that will run every time the second thing happens aka, whenever the door opens this will run.

This one was just fine, it worked, whee me: now i was ready for something a little more complicated.

Spoiler: oh hell no.

This was my goddamn Waterloo; after pulling all the code templates and reading them, I found out that a: no one even tries to do this the same way and b: I hate everything.

Each def is global to the app, awesome. Internal variables--inside those defs--are defs but not global; got it. Everything has to be triggered by an event: okay, great. This is easy enough if all you want to do is turn on light when contact sensor says open: you can follow it. The switchHandler isn't necessary at all, actually; the only reason it's there is for logging purposes. I didn't even have to subscribe to that if I didn't want to; all I needed was 'switch1.on()' and that one doesn't need to be defined by me, it's a built in. Variable + action: sweet.

I didn't know that, though; all the scripts had a subscribe for every fucking device and I figured it had to be there or nothing would work. So when I wanted to add a what to do if the door is closed, I added the following to the existing initialize:

subscribe (contact1, "contact.closed", contactClosedHandler)

And the following def:


def contactClosedHandler(evt) {
   log.debug $evt.name: $evt.value"
    switch1.off()
}


Now, every time the door opens the light comes on, and every time its closed it turns off. Awesome.

Then I wanted to add the super obvious: what to do if the door was left open and got that (an if statement with a runIn). But not now: that's for later, because I got confident and stupid and jumped to Level Fucking 10 or something and decided to do a three part automation of my bathroom with a timer. It would turn on, turn off after a user specified time, it even had a shower mode so it wouldn't turn off when using the shower. It had bells, whistles, subDefs, it was beautiful: it would even change the temperature of the lights to 'warm' from 'daylight'. You could pick the dim level.

This was destined to fail.

Two hundred and forty-one lines of code, the lights would come on and go off but sometimes not and on occasion not at all, dim at unexpected and frankly weird times, which was super awkward when using the bathroom. I reverted to a public smartapp, hated everything, and walked away: nothing would work.

I came back to it last week and sat down to glare at it, and then paused in a kind of existential horror because in three minutes glaring, I saw every problem. Two more minutes, I knew exactly where I went wrong and a tentative fix.

I went to look at the templates at github and confirmed the problems, grabbed some script bits to test, confirmed my guess, and in one day, rewrote it to do what I wanted. By the end of the weekend, I'd added in a variation for color bulbs and a moisture sensor for the shower to leave the lights on while showering. (I also added some cool shit in preferences but beside the point.)

What is getting to me is that why this makes perfect sense now--well enough I knocked out a three-device script with fucking light bulb options--but it baffled me a year ago. It's not like I did careful studying of the groovy language between then and now; just for reasons goddamn random, now it makes perfect sense: what doesn't make sense to me anymore is all the mistakes I made in that first bathroom script.



I have existential coding crises. It happens.

Below cut is the full bathroom automation script if you're curious what it looks like. Those that start with 'private' are apparenty standard SmartThings for certain options that you haev to add manually if you use either multiple pages or dynamic pages for your preferences.



/**
 *  Copyright 2019 Seperis
 *
 *  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.
 *
 *  Bathroom Automation III
 *
 *  Author: Seperis
 */

definition(
    name: "Bathroom Automation III",
    namespace: "seperis",
    author: "Seperis",
    description: "Bathroom automation.",
    category: "Convenience",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png"
)

preferences {
    page(name: "page0")
}

def page0() {
	dynamicPage(name: "page0", title: "Configuration", hideWhenEmpty: true) {
		section("Select the sensors you wish to use"){
			input "motion1", "capability.motionSensor", title: "Which motion sensor?", required: true
    		input "moisture1", "capability.waterSensor", title: "Which moisture sensor?", required: false, submitOnChange: true
    	}
    	section("Select the lights you want to control", hideWhenEmpty: true){
			input "switches", "capability.switch", multiple: true, submitOnChange: true
            input "switchConfig", "bool", description: "", title: "Do you want to configure these lights?", hideWhenEmpty: !switches, defaultValue:false, submitOnChange: true
        }
        if (switchConfig){
    		def colLights = []
            def tempLights = []
            def oLights = []
        	switches.each {
        		if (it.hasCapability("Color Control")!= null) {
            		colLights.add(true);
                }else if (it.hasCapability("Color Temperature")!=null){
                	tempLights.add(true);
        		}else{
                	oLights.add(true);
                }
    		}
            section("Configure lights") {
        		if(!oLights){
                	if(!tempLights){
                		input "colorTemp1","enum",description:"",title: "Select color", hideWhenEmpty: isTrue, submitOnChange: true, options: [ "white","red", "orange", "yellow", "green", "blue", "purple", "pink"]
                    }
                    if(!colLights || colorTemp1=="white"){
						input "switchTemp1", "enum", description: "", title: "Select color temperature", hideWhenEmpty: isTrue, defaultValue: 4100, options: [
        				[2700:"Soft White"], [3300:"White"], [4100:"Moonlight"],[5000:"Cool White"],[6500:"Daylight"]]
                    }
            	}
				input "switchLevel1", "enum", description: "", title: "Set dimmer level", defaultValue: 100, options: [
        			[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]]	
        	}
        }
    	section("Other actions"){
    		input "motionStop", "bool", title: "Do you want to turn off the light when motion stops?", required: true, defaultValue:false, submitOnChange: true
    		if(motionStop){
				input "lightTime", "number", title: "How many minutes?", required: false
                if(moisture1){
            		input "showerCheck","bool", title: "Do you want to use the moisture sensor to determine if someone is showering?  The lights will remain on until moisture sensor is dry.", defaultValue:false, submitOnChange: true
                }
        	}
    	}
        section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
            label title: "Assign a name", required: false
			input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
				options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
			input "modes", "mode", title: "Only when mode is", multiple: true, required: false
		}
        section ("Version 1.3") {}
    }
}

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

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

def initialize() {
    log.debug "Initializing"
    subscribe(motion1, "motion", motionHandler)
    subscribe(moisture1, "water", moistureHandler)
}

def motionHandler(evt) {
    log.debug "$evt.name: $evt.value"
    if (evt.value == "active") {
	log.debug "turning on lights"
	switches.on()
    }else if (evt.value == "inactive") {
    	log.debug "there is no motion"
        if(motionStop){
            if(showerCheck){
            	showerRules()
            }else{
            	motionRules()
            }
        }
    }
}

def moistureHandler(evt) {
    log.debug "$evt.name: $evt.value"
    if (evt.value == "dry") {
    log.debug "no moisture detected; go to motionRules"
	motionRules()
    }else{
        log.debug "someone is showering or bathing; do nothing"
    }
}

def motionRules(){
    log.debug "motion rules in effect"
    runIn(lightTime * 60, scheduleCheck, [data:[delayTime:lightTime]])
}

def showerRules(){
    log.debug "shower rules in effect"
    def moistureState=moisture1.currentState("water")
    if (!moistureState || moistureState.value == "dry"){
    	log.debug "no moisture detected; go to motionRules"
	motionRules()
    }else{
        log.debug "someone is showering or bathing; do nothing"
    }
}

def scheduleCheck(data) {
    log.debug "schedule check"
    def motionState = motion1.currentState("motion")
    def moistureState = moisture1.currentState("water")
    def thresholdTime = data.delayTime
    if (motionState.value == "inactive") {
        def elapsed = now() - motionState.rawDateCreated.time
    	def threshold = 1000 * 60 * thresholdTime - 1000
    	if (elapsed >= threshold) {
            if(!showerCheck || !moistureState || moistureState.value == "dry"){
        	if(!showerCheck){
            	    log.debug "Motion has stayed inactive long enough since last check ($elapsed ms) and user did not configure for shower: turning lights off"
        	}else if (!moistureState) {
            	    log.debug "Motion has stayed inactive long enough since last check ($elapsed ms) and shower has not been used: turning lights off"
                }else if (moistureState.value == "dry"){
                    log.debug "Motion has stayed inactive long enough since last check ($elapsed ms) and shower is over: turning lights off"
                }
            	switches.off()
            } else {
            	log.debug "Motion has stayed inactive long enough since last check ($elapsed ms) but moisture sensor is wet: do nothing."
            }
    	} else {
            if (!showerCheck){
                log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms) and user did not configure for shower: do nothing"
            }else if (!moistureState) {
        	log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms) and shower has not been used: do nothing."
            }else if (moistureState.value == "dry"){
            	log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms) and shower is in use: do nothing."
            }
        }
    } else if (motionState.value == "active"){
    	log.debug "Motion is active, do nothing and wait for inactive"
    } else {
    	log.debug "There is a problem with motion sensor"
    }
}

private getAllOk() {
	modeOk && daysOk && timeOk
}

private getModeOk() {
	def result = !modes || modes.contains(location.mode)
	log.trace "modeOk = $result"
	result
}

private getDaysOk() {
	def result = true
	if (days) {
		def df = new java.text.SimpleDateFormat("EEEE")
		if (location.timeZone) {
			df.setTimeZone(location.timeZone)
		}
		else {
			df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
		}
		def day = df.format(new Date())
		result = days.contains(day)
	}
	log.trace "daysOk = $result"
	result
}

private getTimeOk() {
    def result = true
    if (starting && ending) {
    	def currTime = now()
    	def start = timeToday(starting, location?.timeZone).time
    	def stop = timeToday(ending, location?.timeZone).time
    	result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
    }
    log.trace "timeOk = $result"
    result
}
    
private hhmm(time, fmt = "h:mm a"){
	def t = timeToday(time, location.timeZone)
    def f = new java.text.SimpleDateFormat(fmt)
    f.setTimeZone(location.timeZone ?: timeZone(time))
    f.format(t)
}
    
private getTimeIntervalLabel(){
	(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}
    

private hideOptionsSection() {
	(starting || ending || days || modes) ? false : true
}





Edited to add pre and code markup.

Posted at Dreamwidth: https://seperis.dreamwidth.org/1042432.html. | You can reply here or there. | comment count unavailable comments