Let's model the change

Short Version:

This community of developers can and should model the change they desire to see.


Longer Version:

It occurs to me that there are several talented developers in here that all have ideas, some brilliant and some not so much, but all have value and could provoke others into thought/creation. For the last year+ HE staff have put forth several improvements in the UI but I haven't seen a lot of developers throwing what I'd call "fleshed out" examples of what they've comptemplated or tried. So a challenge for any who would like to participate:

As developers we can either go with the default out of the box interface options, or, using the tools that we've been given, develop new ones that may be something that others grab onto and illustrate options that could influence future direction; i.e. stop saying you want a better UI, or process, or... build a model of what you think works better and put it out there for discussion. Doesn't have to be fully functional, just enough to illustrate your idea and spur others to think and comment. (If possible it would be appreciated if comments followed the convention of pointing out the positives of the approach before giving constructive criticism.)


I'll start. A couple weeks ago one of the community was lamenting that the Device UI was too rigid and expressed that he/she disliked the necessity of using tabs to access various elements. This post got me to thinking about alternatives and since I had a little time to play I started digging into posts on CSS, Javascript and HTML and put together a small demo app:

https://raw.githubusercontent.com/thebearmay/hubitat/refs/heads/main/development/deviceUI.groovy

A couple of caveats:

  1. This is late Alpha code designed to illustrate an idea
  2. To speed up the generation of a protype I have hardcoded some items that should be abstracted before publishing a Beta framework
  3. While the prototype target is the device UI (because that is the conversation that got me playing) I'm pretty sure I could develop a similar framework that would allow multiple apps to be presented in similar fashion
  4. UI is not my strong suit :sunglasses:

Highlights:

  • Multiple draggable regions (grab the title bar)
  • Regions are resizable (handle is in the lower right corner of each)
  • Single click collapse/restore
  • Region placement and data is preserved between sessions
  • Works on mobile and desktop
  • Title bar changes color to alert to pending changes that have not been committed

Okay go....

26 Likes

This is complex. So, must have taken you an extra 5 minutes to throw it together?!? :wink: Sorry, I didn't mean to insult your abilities. 4 minutes?

5 Likes

:joy: Took me a little longer than I thought it should (actual working time is around 40-50 hours but I haven't been in my home office a lot the last 2-3 weeks), and it still needs some work before it's production ready, but I've enjoyed the learning experience. If we decide it's something worth pursuing as a model I would want to convert the generic elements to a library (it's almost there) and provide some simple methods to instantiate the regions.

6 Likes

Very nice. First thought that comes to mind: a "maximize" button next to the "minimize" on each region; be especially nice for a quick zoom-out on "wide" regions like Events and Scheduled Jobs.

Also: the "switch" for Hub Info isn't working exactly right.

Not a bad idea...

Can you elaborate on this?

On the device I tried it on, Hub Mesh was enabled, and said "Enabled", but the visible switch was off. Clicking the switch moved the switch, but never changed the "Enabled" label; I'm not sure whether or not it was actually changing the Hub Mesh state.

I'll check into that, switch should reflect the state - verbiage is from the original device interface.

I abstracted a lot of the code and have a uiRegions library available at:

https://raw.githubusercontent.com/thebearmay/hubitat/refs/heads/main/libraries/uiRegions.groovy

Normally I'd say use #include thebearmay.uiRegions to include the library but at the moment the library implementation is fighting me so you'll need copy/paste it into your code for now.

A working page really only needs two calls to generate the HTML required to implement:

String regionDef = getRegion(String regionName, String  regionTitle, String regionContent )
String getRegionsPage( ArrayList <HashMap> [regionName:regionDef...], bool fullScreen )

While I work on converting my original code from above I wrote a quick demo program that could act as skeleton for anyone who would like to play.

UI Regions Skeleton Code
/*
 * Regions Demo
 * 
 *      Demo application to for the uiRegions library
 *
 *  Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except
 *String   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, WIyTHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *    Date            Who                    Description
 *    -------------   -------------------    ---------------------------------------------------------
 *    
 */
static String version()	{  return '0.0.1'  }
import groovy.transform.Field

//#include thebearmay.uiRegions


definition (
	name: 			"Regions Demo", 
	namespace: 		"thebearmay", 
	author: 		"Jean P. May, Jr.",
	description: 	"Alternate Device UI",
	category: 		"Utility",
	importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/apps/xxx.groovy",
    installOnOpen:  true,
	oauth: 			false,
    iconUrl:        "",
    iconX2Url:      ""
) 

preferences {
    page name: "_mainPage"

}


def installed() {
    state?.isInstalled = true
    initialize()
}

def updated(){
    if(!state?.isInstalled) { state?.isInstalled = true }
	if(debugEnabled) runIn(1800,logsOff)
}

def initialize(){
}

void logsOff(){
     app.updateSetting("debugEnabled",[value:"false",type:"bool"])
}

def _mainPage(){
    dynamicPage (name: "_mainPage", title: "<h2 style='background-color:#e6ffff;border-radius:15px'>${app.getLabel()}<span style='font-size:xx-small'>&nbsp;v${version()}</span></h2>", install: true, uninstall: true) {
        section (name:'cPageHndl', title:''){
            r1Content = "<p>Region 1</p>"
            r2Content = "<div><p style='background:linear-gradient(135deg, #ffffff 0%, #2596be 100%);'>Region 2</p></div>"
            r1 = getRegion('region-1', 'Region 1', "$r1Content")
            r2 = getRegion('region-2', 'Region 2', "$r2Content")
            pContent = getRegionsPage( ['region-1':r1, 'region-2':r2], true)
            paragraph pContent
            
            if(debugEnabled) {
                pname = "uiRegionWork${app.id}.htm"
    		    uploadHubFile ("$pname",pContent.getBytes("UTF-8"))
           }
            
        }
    }
}

/*
 * UI Regions
 * 
 * Library to produce an html block with dragable/resizable regions
 * 
 *
 *  Licensed Virtual 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, WIyTHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *    Date            Who                    Description
 *    -------------   -------------------    ---------------------------------------------------------
 *    
 */

import java.text.SimpleDateFormat
import groovy.transform.Field


library (
    base: "app",
    author: "Jean P. May Jr.",
    category: "UI",
    description: "Set of methods that allow the customization of the UI ",
    name: "uiRegions",
    namespace: "thebearmay",
    importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/refs/heads/main/libraries/uiRegions.groovy",
    version: "0.0.1",
    documentationLink: ""
)

String getRegion(regionName, regionTitle, regionContent){

	String region = """<div class="region" id="${regionName}">
            <div class="region-header">
                <div class="region-title">${regionTitle}</div>
				<span style='display:inline-table'>
					<span class="toggleBtn" onclick="toggleRegion(this)" ontouchstart="toggleRegion(this)">-</span>
					<span class="maxRstBtn" onclick="maxRstRegion(this)" ontouchstart="maxRstRegion(this)">&#9713;</span>
				</span>
            </div>
            <div class="region-content">
				${regionContent}
			</div>
            <div class="resize-handle se"></div>
        </div>
		"""
		
	return region

}

String getRegionsPage( regionsList, fullScreen ){
	// regionsList should be a list of map elements [regionName:regionContentString]
	String regionsMerged = ''
	String dragList = ''
	String defaultPos = ''
	int regionsInx = 0
	int l = 50
	int t = 0
	int w = 300
	int h = 250
	regionsList.each {
		regionsMerged += it.value
		if(regionsInx > 0) {
			dragList += ','
			defaultPos += ','
		}
		dragList += "'${it.key}'"
		defaultPos += "'${it.key}': { left: '${l}px', top: '${t}px', width: '${w}px', height: '${h}px', zIndex: '${regionsInx+1}' }"
		t+= 44
		l+= 30
		regionsInx++
	}
    
	String bodyHtml = """
<div>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: linear-gradient(135deg, #ffffff 0%, #2596be 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .regContainer {
			position: relative;
            width: 100%;
            height: calc(100vh - 40px);
        }

		.toggleBtn {
			width: 1em;
    		background: none;
		    border: none;
		    color: white;
    		cursor: pointer;
    		font-size: 16px;
    		padding: 0 5px;
    		line-height: 1;
    		font-weight: bold;
		}

		.toggleBtn:hover {
    		color:red;
		}
		
		.maxRstBtn {
			width: 1em;
    		background: none;
		    border: none;
		    color: white;
    		cursor: pointer;
    		font-size: 16px;
    		padding: 0 5px;
    		line-height: 1;
    		font-weight: bold;
		}

		.maxRstBtn:hover {
    		color:red;
		}

		.region.collapsed .region-content {
    		display: none;
		}

		.region.collapsed .resize-handle {
    		display: none;
		}

        .region {
			touch-action: none;
            position: absolute;
            background: white;
            border-radius: 8px;
			border: 1px solid gray;
            box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
            overflow: hidden;
            min-width: 200px;
            min-height: 44px;
			line-height:1.25em;
            transition: box-shadow 0.2s;
        }

		.region {
    		display: flex !important;
    		flex-direction: column !important;
    		padding: 0 !important;
		}

		.region-content {
    		flex: 1;
    		overflow: auto;
			padding-left:5px;
		}

        .region:hover {
            box-shadow: 0 10px 15px rgba(0, 0, 0, 0.15), 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .region.active {
            box-shadow: 0 20px 25px rgba(0, 0, 0, 0.2), 0 10px 10px rgba(0, 0, 0, 0.15);
        }

        .region.collapsed {
            height: 44px !important;
        }
		
		.region.fullScreen {
            top: 0px !important;
			height: 95vh !important;
			min-width: 80vw;
			left: 0px !important;
        }

        .region-header {
			-webkit-user-select: none;
			touch-action: none;
            background: linear-gradient(135deg, #2596be 0%, #bbbbcc 100%);
            color: white;
            padding: 0px 15px;
            cursor: move;
            user-select: none;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

		
		.region-header-dirty {
			background: linear-gradient(135deg, #ff9625 0%, #ccbbbb 100%);
		}

        .region-title {
            font-weight: 600;
            font-size: 14px;
        }

		.region-subheader {
			font-size:16px;
			font-weight:bold;
			text-decoration:underline;
		}

		.region-content {
            padding: 0px 15px;
            height: calc(100% - 44px);
            overflow: auto;
            transition: opacity 0.2s;
        }

        .resize-handle {
			touch-action: none;
            position: absolute;
            background: transparent;
        }

        .resize-handle.se {
            bottom: 0;
            right: 0;
            width: 44px;
            height: 44px;
            cursor: nwse-resize;
        }

        .resize-handle.se::after {
            content: '';
            position: absolute;
            bottom: 2px;
            right: 2px;
            width: 12px;
            height: 12px;
            border-right: 2px solid #cbd5e0;
            border-bottom: 2px solid #cbd5e0;
        }

        .reset-btn {
            //position: fixed;
            bottom: 20px;
            //right: 20px;
            background: white;
            color: #667eea;
            border: none;
            padding: 10px 20px;
            border-radius: 6px;
            font-weight: 600;
            cursor: pointer;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            transition: all 0.2s;
        }

        .reset-btn:hover {
            background: #667eea;
            color: white;
            transform: translateY(-2px);
            box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
        }



    </style>
	<script>
		function toggleRegion(btn) {
			event.stopPropagation()
		    const region = document.getElementById(btn.parentElement.parentElement.parentElement.id);
			if(region.classList.contains('collapsed')){
				//alert("case collasped")
				btn.innerText = '-';
				region.classList.remove('collapsed');
				savePos()
			} else { 
				//alert("case expanded")
				btn.innerText = '+';
				region.classList.add('collapsed');
				region.classList.remove('fullScreen');
				savePos();
			}
		}
		
		function maxRstRegion(btn) {
			event.stopPropagation()
		    const region = document.getElementById(btn.parentElement.parentElement.parentElement.id);
			if(region.classList.contains('fullScreen')){
				region.classList.remove('fullScreen');
				savePos()
			} else { 
				region.classList.add('fullScreen');
				region.classList.remove('collapsed');
				savePos();
			}
		}

		function savePos(){
			saveStr = '';
			const regionList = [${dragList}];
			for(i=0;i<regionList.length;i++){
				tRegion = document.getElementById(regionList[i]);
				const collapsed = tRegion.classList.contains('collapsed') ? '1' : '0';
				const maximized = tRegion.classList.contains('fullScreen') ? '1' : '0';
				saveStr += regionList[i]+':{'+tRegion.style.left+','+tRegion.style.top+','+tRegion.style.width+','+tRegion.style.height+','+tRegion.style.zIndex+ ',' + collapsed+','+maximized+'};';
				//alert("saving "+regionList[i]+":"+collapsed)
			}
			document.getElementById('settings[savePos]').value = saveStr;
			changeSubmit(document.getElementById('settings[savePos]'))
		}
						

		function parseStr(parmStr){
			items=parmStr.split(';')
			for(i=0;i<items.length-1;i++){
				id=items[i].substring(0,items[i].indexOf(':'))
				xyVals=items[i].substring(items[i].indexOf('{')+1,items[i].indexOf('}')).split(',');
				elem=document.getElementById(id);
				elem.style.left = xyVals[0];
				elem.style.top = xyVals[1];
				elem.style.width = xyVals[2];
				elem.style.height = xyVals[3];
				elem.style.zIndex = xyVals[4];
				maxZIndex = Math.max(maxZIndex, parseInt(xyVals[4]) || 0);
				// Restore collapsed state if saved
        		if (xyVals[5] == '1') {
            		elem.classList.add('collapsed');
	            	const btn = elem.querySelector('.toggleBtn');
    	        	if (btn) btn.textContent = '+';
        		} else {
            		elem.classList.remove('collapsed');
            		const btn = elem.querySelector('.toggleBtn');
		           	if (btn) btn.textContent = '−';
    	    	}
        		if (xyVals[6] == '1') {
            		elem.classList.add('fullScreen');
					elem.classList.remove('collapsed');
				} else {
            		elem.classList.remove('fullScreen');
    	    	} 
			}


		}

        draggableRegions = [${dragList}];
        var maxZIndex = ${regionsInx};
        
        // Default positions for initial load

		var defaultPositions = {
			${defaultPos}
        };

        // Bring region to front
        function bringToFront(region) {
            maxZIndex++;
            region.style.zIndex = maxZIndex;
        }

        // Load saved states on page load 
        //loadRegionState();//- only runs if no saved setting value
        function loadRegionState() {
            draggableRegions.forEach(regionId => {
                region = document.getElementById(regionId);
                defaults = defaultPositions[regionId];
                region.style.left = defaults.left;
                region.style.top = defaults.top;
                region.style.width = defaults.width;
                region.style.height = defaults.height;
                region.style.zIndex = defaults.zIndex;
            });
        }
      
	draggableRegions.forEach(regionId => {
			const region = document.getElementById(regionId);
			const header = region.querySelector('.region-header');
			const resizeHandle = region.querySelector('.resize-handle.se');

			let isDragging = false;
			let isResizing = false;
			let currentX, currentY, initialX, initialY;
			let initialWidth, initialHeight, resizeStartX, resizeStartY;

			function getClient(e) {
				return e.touches ? e.touches[0] : e;
			}

			region.addEventListener('mousedown', () => bringToFront(region));
			region.addEventListener('touchstart', () => bringToFront(region), { passive: true });

			function onDragStart(e) {
				if (e.target === resizeHandle || resizeHandle.contains(e.target)) return;
				e.preventDefault();
				isDragging = true;
				const client = getClient(e);
				initialX = client.clientX - (parseInt(region.style.left) || 0);
				initialY = client.clientY - (parseInt(region.style.top) || 0);
				region.classList.add('active');
				bringToFront(region);
			}

			function onDragMove(e) {
				if (!isDragging) return;
				e.preventDefault();
				const client = getClient(e);
				currentX = client.clientX - initialX;
				currentY = client.clientY - initialY;
				region.style.left = currentX + 'px';
				region.style.top = currentY + 'px';
			}

			function onDragEnd() {
				if (isDragging) { isDragging = false; savePos(); }
			}

			header.addEventListener('mousedown', onDragStart);
			header.addEventListener('touchstart', onDragStart, { passive: false });
			document.addEventListener('mousemove', onDragMove);
			document.addEventListener('touchmove', onDragMove, { passive: false });
			document.addEventListener('mouseup', onDragEnd);
			document.addEventListener('touchend', onDragEnd);

			function onResizeStart(e) {
				e.stopPropagation();
				e.preventDefault();
				isResizing = true;
				const client = getClient(e);
				resizeStartX = client.clientX;
				resizeStartY = client.clientY;
				initialWidth = region.offsetWidth;
				initialHeight = region.offsetHeight;
				region.classList.add('active');
			}

			function onResizeMove(e) {
				if (!isResizing) return;
				e.preventDefault();
				const client = getClient(e);
				const width = initialWidth + (client.clientX - resizeStartX);
				const height = initialHeight + (client.clientY - resizeStartY);
				if (width > 200) region.style.width = width + 'px';
				if (height > 100) region.style.height = height + 'px';
			}

			function onResizeEnd() {
				if (isResizing) { isResizing = false; savePos(); }
			}

			resizeHandle.addEventListener('mousedown', onResizeStart);
			resizeHandle.addEventListener('touchstart', onResizeStart, { passive: false });
			document.addEventListener('mousemove', onResizeMove);
			document.addEventListener('touchmove', onResizeMove, { passive: false });
			document.addEventListener('mouseup', onResizeEnd);
			document.addEventListener('touchend', onResizeEnd);
	});
	</script>
    <div class='regContainer' id='container'>
		${inputHiddenElem(name:'savePos', type:'hidden', width:'1em', radius:'12px', background:'#2596be', title:'', submitOnChange:true, defaultValue:'')}
		${regionsMerged}
    </div>
</div>
"""
     if(settings["savePos"])
    	bodyHtml+="<script>parseStr('${settings["savePos"]}');</script>"
    else 
        bodyHtml+="<script>loadRegionState();</script>"
	if(fullScreen)
		return bodyHtml + fullScrn
	else
		return bodyHtml
}

String inputHiddenElem(HashMap opt) {
    if(!opt.name || !opt.type) return "Error missing name or type"
    if(settings[opt.name] != null){
        if(opt.type != 'time') {
        	opt.defaultValue = settings[opt.name]
        } else {
            SimpleDateFormat sdf = new SimpleDateFormat('HH:mm')
            SimpleDateFormat sdfIn = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
            opt.defaultValue = sdf.format(sdfIn.parse(settings[opt.name]))
        }
    }
    typeAlt = opt.type
    if(opt.type == 'number') {
    	step = ' step=\"1\" '
    } else if (opt.type == 'decimal') {
        step = ' step=\"any\" '
        typeAlt = 'number'
    } else {
        step = ' '
    }
   
	String computedStyle = ''
    if(opt.type == 'hidden'){
        opt.type='text'
        typeAlt = 'hidden'
        computedStyle += 'visibility:hidden;'
    }        
        
    if (opt.float) computedStyle +="float:${opt.float};"
    if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};"
    if(opt.background) computedStyle += "background-color:${opt.background};"
    if(opt.color) computedStyle += "color:${opt.color};"
    if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};"
	if(opt.radius) computedStyle += "border-radius:${opt.radius};"
    if(!opt.multiple) opt.multiple = false
    
    if(opt.hoverText && opt.hoverText != 'null'){  
    	opt.title ="${opt.title}<div class='tTip'> ${btnIcon([name:'fa-circle-info'])}<span class='tTipText' style='width:${opt.hoverText.size()/2}em'>${opt.hoverText}</span></div>"
    }
    String retVal = "<div class='form-group'><input type='hidden' name='${opt.name}.type' value='${opt.type}'><input type='hidden' name='${opt.name}.multiple' value='${opt.multiple}'></div>"
	retVal+="<div class='mdl-cell mdl-cell--4-col mdl-textfield mdl-js-textfield has-placeholder is-dirty is-upgraded' style='' data-upgraded=',MaterialTextfield'>"
    retVal+="<label for='settings[${opt.name}]' style='min-width:${opt.width}' class='control-label'>${opt.title}</label><div class='flex'><input type='${typeAlt}' ${step} name='settings[${opt.name}]' class='mdl-textfield__input submitOnChange' style='${computedStyle}' value='${opt.defaultValue}' placeholder='Click to set' id='settings[${opt.name}]'>"
    retVal+="<div class='app-text-input-save-button-div' onclick=\"changeSubmit(document.getElementById('settings[$opt.name]'))\">"
    if(typeAlt != 'hidden')
    	retVal +="<div class='app-text-input-save-button-text'>Save</div><div class='app-text-input-save-button-icon'>⏎</div>"
    retVal +="</div></div></div>"
    return retVal
}

@Field static String fullScrn = "<script>document.getElementById('divSideMenu').setAttribute('style','display:none !important');document.getElementById('divMainUIHeader').setAttribute('style','height: 0 !important;');document.getElementById('divMainUIContent').setAttribute('style','padding: 0 !important;');document.getElementById('divMainUIFooter').setAttribute('style','display:none !important');contentHeight = Math.round(window.innerHeight * 1.2);document.getElementById('divMainUIContentContainer').setAttribute('style', 'background: white; height: ' + contentHeight + 'px !important;');document.getElementById('divLayoutControllerL2').setAttribute('style', 'height: ' + contentHeight + 'px !important;');</script><style>overflow-y: scroll !important;</style>"	
1 Like

This is a great idea, and I hope it gets some momentum. Sadly, my skill set limits me to bitching, and whining about the current UI.

4 Likes

This is the first time I've seen HTML, JS and CSS baked into a custom app, yet I think I've been paying attention for the last 8 years. Made me wonder: Was that in the Docs? Did staff post working examples on Github? Is it even officially sanctioned?

That right there tells me the lines of communication (about what's possible at the dev level) could be opened further for those interested. Specifically, to your point about offering constructive input:

Prospective HE developers need more in terms of a built-in, full-featured IDE suite with code auto-fill, prototyping/stubs, templating, CSS class & UI object library, API & I/O entry points, collaboration tools, the whole enchilada. Not saying it needs to live on the hub, but bonus points if it could.

If that strikes anyone as an over-the-top request, I'd quickly point out that while some of us (wink, wink) are exceedingly good at reverse engineering stuff, some are masters of documentation (looking at @joshlobe, @garyjmilne, @csteele ...), others are able to plumb various wikis for clues, while others still manage to pull things out of a hat (remember the first time someone demo'd drag-and-drop for legacy dash tiles? oooooh).... ponder the greatness that might come from genuine team builds.

Kudos @thebearmay for putting this out there, both in terms of presenting it as a group effort and taking the initiative to show what's possible. Guys like me have been standing in the wading pool too long.

8 Likes

Know about this??

Two years old, mighta missed it.

2 Likes

My breakthrough was delivering JS from within a groovy app on an endpoint. CSS, HTML and SVG were all pretty easy but once you add JS it opens up a whole new realm of possibilities. Remote Builder is my best example of this. (This kind of thing). Once the JS is initialized you are only passing data back an forth, not structure.

I did try the Intellij interface a while back but I found the performance of saving to be somewhat slow compared to the native interface. The other thing that was a pain was the hundreds of error messages which the anal programmer in me wanted to reduce to 0 even when some of the "errors" were misinformed.

6 Likes

Honestly the community made VSCode plugin is better as far as how it publishes the code and keeps track of your hub in the settings. The IntelliJ plugin requires that you add comments to the top of your code with your hubs info, which is very silly IMO. The only thing the Hubitat IntelliJ plugin does better is that it posts the code much faster. I believe it uses some sort of newer API/Endpoint that was not available when the VS Code plugin was made.

4 Likes

+1000
Using JS in my rule machine manager app. Not sure it would be possible without it.

2 Likes

Whole heartedly agree.

There's a VSCode plugin? (facetious) :slight_smile:

Here is info. I think it is published in the plugin library as well so you might be able to just search for it. Hubitat Developer VSCode Plugin

2 Likes

So, back to @thebearmay 's original premise... Devs doing devvy things.

Your example certainly sparks my imagination. My first thought? AppMaker™ -- a multi-pane pseudo-IDE comprising:

  • Scrollable list of clickable buttons that paste sample code (e.g. Driver Header that spits out boilerplate which author fills in, or Log Event for a code block that handles various Logging levels, etc.) along with every allowable #include library;
  • Central editing textbox where all the Groovy stuff builds
  • Menu bar for Open, Save, New, Cut/Copy, Import, etc.;
  • Docs pane that offers dynamic real-time links to actual Docs (or simply brief explainers) covering a highlighted term on-screen;
  • Filelist showing existing resource files (Groovy, JSON, TXT, etc.) from File Manager, with corresponding URL for any highlighted file (helpful when later importing finished code into Drivers/Apps Code areas);

Stretch goals include features like syntax checking (tall order), line numbering (easy), commenting on/off (iffy), URL-decoding/encoding, keyword color-coding, and so forth.

Future visions:

  • HEquery - a community Library of quick-and-dirty snippets offered by devs willing to contribute code for others to leverage;
2 Likes

For anyone who is interested I have updated the code at https://raw.githubusercontent.com/thebearmay/hubitat/refs/heads/main/development/deviceUI.groovy to use the new library and have removed all of the hard coding.

3 Likes