What did you build/automated today (with pictures)?

I’ve been playing with WLED a fair bit for accent lighting in my garage, but ventured into the 2D Matrix aspec of WLED to build my own LED display to show some information directly fed from openhab.

I poll weather conditions from my weatehr station into openhab and then present that information on the display i been building.




Lastly doing a fabric wrap test:

Now that’s what I call a garage :heart_eyes:

Here is my OH RPi on a custom built 19" rack..

I have integrated my solar controller CosmoUno into Openhab4,
using a VBus decoder on a Raspberry Pi Zero.
For more information, feel free to DM me:

This thread is desperately in need of new content - can’t it be “highlighted” once a month to try and get the users attention for example?

Anyway I’ll do my part :smiley:
Here’s my ongoing project: a Stratum 1 local NTP server running on Esphome:
IMG_5286
IMG_5285
IMG_5282 Copy

After the stupid power outage that hit the south of France, Spain and Portugal, I’ve been thinking about small projects I can add to OpenHAB so that if the power is out I’d be able to continue limping on for an extended period of time.
Solar panels and batteries are already approved by management (wife) but having a local NTP server is a low hanging fruit, and if you’re out of internet, after your phone loses power I have no idea how it would deal with the date and time.
So here comes my idea: take some hints from off grid meshtastic nodes and add a gps module to pull the date and time directly off the satellites :slight_smile:

Right now it’s sticking on the window but as I have some time available I’ll update this :slight_smile:

Edit:
Uodates!
Added a 5 watt solar panel;
Added an mppt to control the charge of a battery;
Added a 3.7v lipo battery;
Added an INA219 to measure the solar panel energy production;
Esphome provides all of the needed information related to the mentioned above.

IMG_5767
IMG_5711
IMG_5678

I’ll post another one to show that hardware doesn’t always have to be involved. Traffic in my area has grown worse and worse over the years I’ve lived here, and I have a ten-minute window in which to drop my kid off to school (7:50am to 8:00 am). Some mornings I’ll leave at 7:20 and we end up sitting at the school for ten minutes. Other mornings I’ll leave at the same time and the kid gets a tardy notice because there was an accident on the interstate.

My solution involves multiple parts.

  1. A rule to poll Waze’s API to get how long the drive is taking right now. You can find the rule as a rule template at Driving Time and Distance using Waze [4.0.0.0;5.9.9.9]. This will populate a Number:Time Item with the amount of time it’s expected to take to drive and a Number:Length Item for the distance. Why the distance? Because if the distance changes from normal, it means Waze is taking me a different route from normal which means traffic is really backed up, and I need to pay attention that I don’t just start heading for the interstate. Note that it’s up to you to set the rule triggers for this rule. Please do not hammer the Waze APIs unnecessarily. I have mine configured with a cron expression and rule conditions to only run once every five minutes between 6:45 and 7:30 on school days.

  2. I have a second rule that triggers when the time Item changes that waits until the arrival time is after 7:49 and sends a notification and a TTS announcement on the smart speakers. I’ve a further limit that it does not announce more than once every ten minutes which means right now it only announces once but when school starts up again I will play with that again.

configuration: {}
triggers:
  - id: "2"
    configuration:
      itemName: School_TravelTime
    type: core.ItemStateUpdateTrigger
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript
      script: >
        function getTime(inst) {
          let hour = inst.hour();
          let meridian = "AM"
          if(hour == 12) meridian = 'PM';
          if(hour > 12) {
            hour -= 12;
            meridian = 'PM'
          }
          
          return hour + ":" + String(inst.minute()).padStart(2, '0') + " " + meridian;
        }


        var notificationId = "aaclDriveTime";


        items.School_CheckTime.postUpdate(time.toZDT());


        var currArrivalTime = time.toZDT(items.School_TravelTime);
          
        // Update the item

        var msg = "As of " + getTime(time.toZDT()) + " if you leave for school
        now, you will arrive at " + getTime(currArrivalTime);

        items.School_TravelMessage.postUpdate(msg);


        var timeSinceLast =
        time.Duration.between(cache.private.get("lastReport", () =>
        time.toZDT().minusDays(1)), time.toZDT());

        console.debug("Time since last report is " + timeSinceLast);

        if(currArrivalTime.isAfter(time.toZDT("7:49 am")) &&
        timeSinceLast.compareTo(time.Duration.ofMinutes(10)) > 0) {
          console.info(msg);
          cache.private.put("lastReport", time.toZDT());

          // Announce it's time to leave
          actions.Voice.say(msg, null, "chromecast:audiogroup:21632c10-9553-4136-80c3-21202c34e900"); // all speakers group
          
          // Send the notification
          actions.notificationBuilder(msg).addUserId('rlkoshak@gmail.com')
                                          .withTitle('Time to leave!')
                                          .withIcon('f7:car-fill')
                                          .withOnClickAction('app:android=com.waze')
                                          .withReferenceId(notificationId)
                                          .send();
          
          cache.private.put("timer", actions.ScriptExecution.createTimer(time.toZDT("PT20M"), () => {
            actions.notificationBuilder('cancel notification').withReferenceId(notificationId).hide().send();
          }))
        }
    type: script.ScriptAction

  1. Finally, I’ve a widget that shows the current traffic conditions. It also shows a caption under the map which is the contents of that School_TravelMessage. This gets updated every time the travel time is calculated so I can visually check and quickly see what the commute is going to look like before the announcement.

image

Note the pin is roughly halfway between my home and the school to center the map where I can see the traffic over the roads I need.

This widget is on the marketplace at Embedded Waze Live Traffic Map Widget. There is also a Google Maps version at Embedded Google Maps Widget which is a good choice if you only care about the one route between two points instead of the general traffic conditions over an area. The Google Maps version requires a Google Developer account and API key. Waze’s API is open and doesn’t require anything.

image

With 1 through 3 combined I’m able to get a heads-up in the morning and easily check when I need to leave to arrive on time without it being annoying.

Sometimes it’s the little things like this that make all the difference.

Absolutely fantastic!!

Toto toilet seat washlet.
I integrated the toilet seat via a RM4 mini IR blaster.
I created rules to turn off the auto seat opener during the day as the bathroom gets used a lot so I only enable the auto opening at night.
Another rule turns the seat heating off overnight.

I also made a widget to control the main features and added that to the floor plan.

Screenshot from 2025-07-24 09-00-19
Screenshot from 2025-07-24 09-00-46

All works well.

Today I’ve created a command to manage a clothes dryer. It uses the following elements:

  1. The device mentioned in this post ESP32 with a small display to command a smart plug

  2. Two blockly rules, one to switch on the plug, and another to send alerts (to telegram and above’s device)

Blockly script to Switch on the dryer

var itemMetadata;


if (items.getItem('Comando_Secador_de_Roupa_Button_2').state == 'SINGLE') {
  itemMetadata = items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire');
  itemMetadata = (itemMetadata === null) ? { value: '', configuration: {} } : itemMetadata;
  itemMetadata.value = '1h0m0s,command=OFF';
  items.metadata.replaceMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire', itemMetadata.value, itemMetadata.configuration);
} else if (items.getItem('Comando_Secador_de_Roupa_Button_2').state == 'DOUBLE') {
  itemMetadata = items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire');
  itemMetadata = (itemMetadata === null) ? { value: '', configuration: {} } : itemMetadata;
  itemMetadata.value = '2h0m0s,command=OFF';
  items.metadata.replaceMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire', itemMetadata.value, itemMetadata.configuration);
} else if (items.getItem('Comando_Secador_de_Roupa_Button_2').state == 'TRIPLE') {
  itemMetadata = items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire');
  itemMetadata = (itemMetadata === null) ? { value: '', configuration: {} } : itemMetadata;
  itemMetadata.value = '3h0m0s,command=OFF';
  items.metadata.replaceMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire', itemMetadata.value, itemMetadata.configuration);
} else if (items.getItem('Comando_Secador_de_Roupa_Button_2').state == 'QUAD') {
  itemMetadata = items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire');
  itemMetadata = (itemMetadata === null) ? { value: '', configuration: {} } : itemMetadata;
  itemMetadata.value = '4h0m0s,command=OFF';
  items.metadata.replaceMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire', itemMetadata.value, itemMetadata.configuration);
} else if (items.getItem('Comando_Secador_de_Roupa_Button_2').state == 'PENTA') {
  itemMetadata = items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire');
  itemMetadata = (itemMetadata === null) ? { value: '', configuration: {} } : itemMetadata;
  itemMetadata.value = '5h0m0s,command=OFF';
  items.metadata.replaceMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire', itemMetadata.value, itemMetadata.configuration);
}
items.getItem('Tomada_Secador_Roupa_Output_Switch').sendCommand('ON');

Blockly script to send alerts

if (items.getItem('Tomada_Secador_Roupa_Output_Switch').state == 'ON') {
  items.getItem('Comando_Secador_de_Roupa_Status').sendCommand((time.ZonedDateTime.now().plusHours(parseFloat(((items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire') !== null) ? (items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire').value) : 'undefined').slice(0, 1))).format(time.DateTimeFormatter.ofPattern('HH:mm'))));
  items.getItem('aAlert').sendCommand(('Secador de roupa ligado durante ' + String(((items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire') !== null) ? (items.metadata.getMetadata('Tomada_Secador_Roupa_Output_Switch', 'expire').value) : 'undefined').slice(0, 2))));
} else {
  items.getItem('Comando_Secador_de_Roupa_Status').sendCommand('OFF');
  items.getItem('aAlert').sendCommand('Secador de roupa desligado');
}

Pressing button2 (green) switches on the dryer for a number of hours (=number of clicks) and displays the end time.

For example, after 3 clicks the dryer switches on during 3 hours and the command displays

imagem

Today I’ve implemented an ir blaster to control some candles. I’ve used a Moes UFO-R11 connected to the zigbee network. The UFO-R11 is hidden behind a decorative tissue but works very well. The candles are +/- 4 meters away.

This was also my first experience with the MQTT library for blockly:

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: ir_velas_switch
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      blockSource: <xml xmlns="https://developers.google.com/blockly/xml"><block
        type="oh_event" id="iji7+~MMawC!Cws`XtDa" x="110" y="194"><field
        name="eventType">sendCommand</field><value name="value"><shadow
        type="text" id="QBNt_|@vzsmfj!!+ts}N"><field
        name="TEXT">value</field></shadow><block type="text"
        id="CTDrV6`f(A%8JRw~?c5w"><field
        name="TEXT"></field></block></value><value name="itemName"><shadow
        type="oh_item" id="e/is$5gmvVaxk5U^IUVL"><mutation itemName="MyItem"
        itemLabel="MyItem"></mutation><field
        name="itemName">MyItem</field></shadow><block type="oh_item"
        id="dx,r72]OJeC45N:BC3(B"><mutation
        itemName="Comando_IR_Velas_IR_Code_to_Send" itemLabel="Comando IR Velas
        IR Code to Send"></mutation><field
        name="itemName">Comando_IR_Velas_IR_Code_to_Send</field></block></value></block><block
        type="controls_if" id="+pNL09|l+[IUtO))q/f*" x="80" y="326"><mutation
        else="1"></mutation><value name="IF0"><block type="logic_compare"
        id="r;5V6O#_}9l{j4jT~I{@"><field name="OP">EQ</field><value
        name="A"><block type="oh_getitem_state" id="41A`#%#UdE)l.~cw#1^^"><value
        name="itemName"><shadow type="oh_item"
        id="p4/:jG5J(FLy7n,m*~u0"><mutation itemName="MyItem"
        itemLabel="MyItem"></mutation><field
        name="itemName">MyItem</field></shadow><block type="oh_item"
        id="dW)^5xo~wxc=qL}lFZwK"><mutation itemName="ir_velas_switch"
        itemLabel="IR Velas Switch"></mutation><field
        name="itemName">ir_velas_switch</field></block></value></block></value><value
        name="B"><block type="text" id="(I#iJUm+wo[7??DUUB?F"><field
        name="TEXT">ON</field></block></value></block></value><statement
        name="DO0"><block type="mqtt_publish" id="$gB{}P|PhBYZp{v`hxC="><field
        name="RETAIN">FALSE</field><value name="MESSAGE"><shadow type="text"
        id="9rB,[(=TPSe2h~4s%]L+"><field
        name="TEXT">message</field></shadow><block type="text"
        id="m`J~QAhNRthx#}p|=rz="><field
        name="TEXT">{"ir_code_to_send":"C5sjiRFfAnYGXwIpAuAVA0Aj4A8DAClgAUAHQAPgBwEEXwJ2BimgA8AL4AUHAQud4DeHBykCdgZfAikCQAPgBwHgARNAIwF2BuADB0ALwAMBXwLgOYcBXwJAAwApYAHAB0AB4AULAnYGKSADwAdAC+AFA+BNhwJfAingAAGACwJ2Bl8gA0AHQAPAC8AHASkC4EGHAV8C4AEDwAFAE4ABAnYGXyAD4AMH4AMLAV8C4E2HAV8C4AcDQGvgCQMJKQJ2BikCdgYpAg=="}</field></block></value><value
        name="TOPIC"><shadow type="text" id="x`rwzAgjQL{uTlnZ|-PJ"><field
        name="TEXT">topic</field></shadow><block type="text"
        id="7f{@-[DA?p$]1}S$S2-b"><field
        name="TEXT">casa/ir-velas/set</field></block></value><value
        name="BROKER"><shadow type="oh_thing" id="d%#|,Q7;T+![={h_~!?F"><field
        name="thingUid">MyThing</field></shadow><block type="oh_thing"
        id="ezN#,QZiF7^7]X:pejw1"><field
        name="thingUid">mqtt:broker:aveiro</field></block></value></block></statement><statement
        name="ELSE"><block type="mqtt_publish" id="]TGm}*Yj]m+zPY:e%J$+"><field
        name="RETAIN">FALSE</field><value name="MESSAGE"><shadow type="text"
        id="9rB,[(=TPSe2h~4s%]L+"><field
        name="TEXT">message</field></shadow><block type="text"
        id="-VjBj-GRTv2pNSgy0k0~"><field
        name="TEXT">{"ir_code_to_send":"CosjkxE9An4GZAI94AgB4AUTQCMBfgbgAwdAC4ADAT0CgBvgDwFAJ0ABQAfgCwMBJp1Ah0BDAmQCPeAMAeABFwF+BuARA0ABQB/gBQHgBTtAAUAj4AgDAgY9Ag=="}</field></block></value><value
        name="TOPIC"><shadow type="text" id="x`rwzAgjQL{uTlnZ|-PJ"><field
        name="TEXT">topic</field></shadow><block type="text"
        id="v{F(t$sCRQ:@Avr[R/8N"><field
        name="TEXT">casa/ir-velas/set</field></block></value><value
        name="BROKER"><shadow type="oh_thing" id="d%#|,Q7;T+![={h_~!?F"><field
        name="thingUid">MyThing</field></shadow><block type="oh_thing"
        id="iP;!2w^eNFp$05n+[HH*"><field
        name="thingUid">mqtt:broker:aveiro</field></block></value></block></statement></block></xml>
      script: >
        var things = Java.type('org.openhab.core.model.script.actions.Things');



        items.getItem('Comando_IR_Velas_IR_Code_to_Send').sendCommand('');


        if (items.getItem('ir_velas_switch').state == 'ON') {
          things.getActions('mqtt', 'mqtt:broker:aveiro').publishMQTT('casa/ir-velas/set', '{"ir_code_to_send":"C5sjiRFfAnYGXwIpAuAVA0Aj4A8DAClgAUAHQAPgBwEEXwJ2BimgA8AL4AUHAQud4DeHBykCdgZfAikCQAPgBwHgARNAIwF2BuADB0ALwAMBXwLgOYcBXwJAAwApYAHAB0AB4AULAnYGKSADwAdAC+AFA+BNhwJfAingAAGACwJ2Bl8gA0AHQAPAC8AHASkC4EGHAV8C4AEDwAFAE4ABAnYGXyAD4AMH4AMLAV8C4E2HAV8C4AcDQGvgCQMJKQJ2BikCdgYpAg=="}', 'FALSE' === 'TRUE');
        } else {
          things.getActions('mqtt', 'mqtt:broker:aveiro').publishMQTT('casa/ir-velas/set', '{"ir_code_to_send":"CosjkxE9An4GZAI94AgB4AUTQCMBfgbgAwdAC4ADAT0CgBvgDwFAJ0ABQAfgCwMBJp1Ah0BDAmQCPeAMAeABFwF+BuARA0ABQB/gBQHgBTtAAUAj4AgDAgY9Ag=="}', 'FALSE' === 'TRUE');
        }
    type: script.ScriptAction

The ir blaster definition is:

UID: mqtt:topic:aveiro:530b99752e
label: Comando IR Velas
thingTypeUID: mqtt:topic
configuration:
  payloadNotAvailable: offline
  payloadAvailable: online
  transformationPattern:
    - JSONPATH:$.state
  availabilityTopic: casa/ir-velas/availability
bridgeUID: mqtt:broker:aveiro
channels:
  - id: LinkQuality
    channelTypeUID: mqtt:number
    label: Link Quality
    configuration:
      unit: lqi
      min: 0
      stateTopic: casa/ir-velas
      transformationPattern:
        - REGEX:(.*linkquality.*)∩JSONPATH:$.linkquality
      max: 255
  - id: BatteryPercentage
    channelTypeUID: mqtt:number
    label: BatteryPercentage
    configuration:
      unit: "%"
      min: 0
      stateTopic: casa/ir-velas
      transformationPattern:
        - REGEX:(.*battery.*)∩JSONPATH:$.battery
      max: 100
  - id: voltage
    channelTypeUID: mqtt:number
    label: Voltage
    configuration:
      stateTopic: casa/ir-velas
      transformationPattern:
        - REGEX:(.*voltage.*)∩JSONPATH:$.voltage
      unit: mV
  - id: learn
    channelTypeUID: mqtt:switch
    label: Learn
    description: ""
    configuration:
      commandTopic: casa/ir-velas/set
      stateTopic: casa/ir-velas/set
      transformationPattern:
        - REGEX:(.*learn_ir_code.*)∩JSONPATH:$.learn_ir_code
      off: '{"learn_ir_code":"OFF"}'
      on: '{"learn_ir_code":"ON"}'
  - id: learned_ir_code
    channelTypeUID: mqtt:string
    label: Learned IR Code
    configuration:
      stateTopic: casa/ir-velas
      transformationPattern:
        - REGEX:(.*learned_ir_code.*)∩JSONPATH:$.learned_ir_code
  - id: ir_code_to_send
    channelTypeUID: mqtt:string
    label: IR Code to Send
    description: ""
    configuration:
      commandTopic: casa/ir-velas/set

There is no need to link any of these channels to items unless you want to control the battery level.

IMG_3717

This is the hub

imagem

Hidden behind this decorative panel (in tissue)

imagem