Use MQTT for Shelly Thermostatic Radiator Valves

So, things have gone well and I have managed to get some more time!

I have to drive my Diverter Valve 'manually' due to the way the boiler was plumbed in, so I still need NR to control the valve. Early days, but a flow was created that has access to five Profile files, these are on the same computer as NR and from here you can download the files both to the TRV and the running profile for NR so that the setpoint can be extracted. This can then be used the same as it has been,in the past - simple comparison >=<

[{"id":"0e47621e9ad92442","type":"comment","z":"0d80ab0dc9580192","name":"http://172.27.123.251/settings/thermostats/0?schedule_profile=1&schedule_rules=0600-0123456-20, 1000-0123456-19, 1500-0123456-21, 1800-0123456-22, 2100-0123456-16","info":"","x":860,"y":40,"wires":[]},{"id":"e55ff2a0097c1601","type":"comment","z":"0d80ab0dc9580192","name":"filepath","info":"// file path with / at the end\nvar filepath = \"/mnt/array1/NodeRed/AutoBackups/\"; // This is the path\nvar localfilepath = \"/home/pi/node-red-storage/\";\n//\"220129_nodered_backup.zip\"","x":370,"y":180,"wires":[]},{"id":"d22324c7f6658b15","type":"inject","z":"0d80ab0dc9580192","name":"","props":[{"p":"filename","v":"Profile1.csv","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"1","topic":"","x":170,"y":60,"wires":[["236e0b92934aa646"]]},{"id":"236e0b92934aa646","type":"file in","z":"0d80ab0dc9580192","name":"Profile","filename":"filename","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":350,"y":220,"wires":[["2c5e99851d92fd90"]]},{"id":"2c5e99851d92fd90","type":"csv","z":"0d80ab0dc9580192","name":"","sep":",","hdrin":false,"hdrout":"none","multi":"mult","ret":"\\n","temp":"","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":470,"y":220,"wires":[["79c4935ebc75c29b"]]},{"id":"2a17101cae4bbbb2","type":"function","z":"0d80ab0dc9580192","name":"Frost/Shower Temps","func":"global.set('tempFrost', 14);\nglobal.set('tempHWAway', 22);\nglobal.set('tempShower', 42); \nglobal.set('tempBoilerDesired', 52);","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":100,"wires":[[]]},{"id":"7c1c4aa16c5ea0de","type":"inject","z":"0d80ab0dc9580192","d":true,"name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.7","topic":"","payload":"","payloadType":"date","x":430,"y":100,"wires":[["2a17101cae4bbbb2"]]},{"id":"79c4935ebc75c29b","type":"function","z":"0d80ab0dc9580192","name":"split file","func":"global.set('profileCurrent', msg.payload[0].col1, \"storeInFile\");\nglobal.set('profileName', msg.payload[1].col1, \"storeInFile\");\n\nlet length = msg.payload.length;\nlet arr = msg.payload.slice(2)\nglobal.set('profileEvents', arr);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":220,"wires":[["a62630aa5523a604","1f06132e71a9db59"]]},{"id":"a62630aa5523a604","type":"debug","z":"0d80ab0dc9580192","name":"debug 118","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":690,"y":180,"wires":[]},{"id":"1f06132e71a9db59","type":"function","z":"0d80ab0dc9580192","name":"set CH time/deg/days","func":"let eventTime;\nlet eventTemp;\nlet eventDays;\nlet length;\nlet eventString = \"\";\nlet arr = msg.payload;\n\nlet profileNumber = arr[0].col1;\nlet profileName = arr[1].col1;\n\nfor (let i = 2; i < arr.length; i++) {\n    eventTime = arr[i].col1;\n    //eventTime = eventTime.toString();\n    eventTemp = arr[i].col2;\n    eventDays = arr[i].col3;\n    eventString = eventString + eventTime + \"-\" + eventDays + \"-\" + eventTemp\n    if (i < arr.length){\n        eventString = eventString + \",\";\n    }\n}\n\nlet messageString = \"http://172.27.123.251/settings/thermostats/0?schedule_profile=\" + profileNumber + \"&profile_name=\" + profileName + \"&schedule_rules=\" + eventString\nmsg.url = messageString;\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":800,"y":220,"wires":[["1ab3183c161818fc","027cbeba6b1c6284"]]},{"id":"1ab3183c161818fc","type":"debug","z":"0d80ab0dc9580192","name":"debug 120","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":890,"y":180,"wires":[]},{"id":"027cbeba6b1c6284","type":"http request","z":"0d80ab0dc9580192","name":"","method":"GET","ret":"txt","paytoqs":false,"url":"","persist":false,"insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":1010,"y":220,"wires":[["be0d47aade8f41b7","2a322bc99fb6d87c"]]},{"id":"be0d47aade8f41b7","type":"debug","z":"0d80ab0dc9580192","name":"debug 121","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1090,"y":180,"wires":[]},{"id":"cef284c5dcd4dac6","type":"inject","z":"0d80ab0dc9580192","name":"","props":[{"p":"filename","v":"Profile2.csv","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"1","topic":"","x":170,"y":100,"wires":[["236e0b92934aa646"]]},{"id":"8d13df490a229a0a","type":"inject","z":"0d80ab0dc9580192","name":"","props":[{"p":"filename","v":"Profile3.csv","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"1","topic":"","x":170,"y":140,"wires":[["236e0b92934aa646"]]},{"id":"4740aa084ccf5b56","type":"inject","z":"0d80ab0dc9580192","name":"","props":[{"p":"filename","v":"Profile4.csv","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"1","topic":"","x":170,"y":180,"wires":[["236e0b92934aa646"]]},{"id":"78bd9619fe660f2e","type":"inject","z":"0d80ab0dc9580192","name":"","props":[{"p":"filename","v":"Profile5.csv","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"1","topic":"","x":170,"y":220,"wires":[["236e0b92934aa646"]]},{"id":"347f7f877f9020c9","type":"catch","z":"0d80ab0dc9580192","name":"","scope":null,"uncaught":false,"x":1080,"y":80,"wires":[["1835d45f1fd96af9"]]},{"id":"1835d45f1fd96af9","type":"debug","z":"0d80ab0dc9580192","name":"debug 98","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1220,"y":80,"wires":[]},{"id":"2a322bc99fb6d87c","type":"function","z":"0d80ab0dc9580192","name":"set CH time/deg/days","func":"delete msg.payload;\ndelete msg.topic;\ndelete msg.filename;\ndelete msg.columns;\n\nlet length;\nlet setHours;\nlet setMinutes;\nlet eventTime;\nlet timeEvent;\nlet arr = [];\narr = global.get('profileEvents');\nlet tempDesired = 27 || 0;\n\n//Get current day start and end timestamp\nlet dateNow = Date.now();\nlet timeFrom = new Date(dateNow).setHours(0, 0, 0, 0);\nlet timeTo = new Date(dateNow).setHours(23, 59, 59, 0)\n\n//Add offset, iterate until file time < current time\nfor (let i = arr.length - 1; i >= 0; i--) {\n    eventTime = arr[i].col1;\n    eventTime = eventTime.toString();\n    msg.test1 = eventTime;\n    setHours = eventTime.slice(0, 2);\n    setMinutes = eventTime.slice(2);\n    timeEvent = new Date(timeFrom).setHours(setHours, setMinutes, 0, 0); //Date from midnight this morning\n    tempDesired = parseFloat(arr[i].col2);\n    msg.tempDesired = tempDesired;\n    msg.dateNow = dateNow;\n    msg.timeEvent = timeEvent;\n\n    if (dateNow > timeEvent) {\n        global.set('tempDesiredCH', tempDesired, 'storeInFile');\n        node.status({ text: timeEvent + \" | \" + tempDesired });\n        return msg;\n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":280,"wires":[["c5980b7598462543"]]},{"id":"c5980b7598462543","type":"debug","z":"0d80ab0dc9580192","name":"debug 119","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":590,"y":280,"wires":[]},{"id":"9dfc059a6c2db66a","type":"inject","z":"0d80ab0dc9580192","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":280,"wires":[["2a322bc99fb6d87c"]]}]

Profiles.zip.txt (1.1 KB)

The profile is only loaded once and also goes to global.context. The times and setpoint are then extracted along with the current time and the setpoint set appropriately.

Comments/coding advice welcome!

Have you thought of using cron-plus for the schedules? That takes care of any timing etc. All you need to do is load the schedule profiles (suitably adjusted for cron-plus).

Having seen what you were doing I thought I could use some of your ideas in a different direction (I also use TRVs, although not Shellys). This is the result, hopefully it will give you some ideas also.

Happy Christmas

A JSON object would be easier to deal with than the CSV, something like this

[
    {
        "profile_name": "Living Room Weekdays",
        "rules": [
            {
                "days": "12345",
                "time": "10:30",
                "temperature": 18
            },
            {
                "days": "12345",
                "time": "14:30",
                "temperature": 20
            },
            {
                "days": "12345",
                "time": "22:30",
                "temperature": 15
            }
        ]
    },
    {
        "profile_name": "Living Room Weekend",
        "rules": [
            {
                "days": "06",
                "time": "11:30",
                "temperature": 18
            },
            {
                "days": "06",
                "time": "14:30",
                "temperature": 20
            },
            {
                "days": "06",
                "time": "22:30",
                "temperature": 15
            }
        ]
    }
]

As an exercise for myself I put together this flow which uses the JSON object above to produce an Object for use with a Shelly TRV (as part of /settings/thermostats/0 [ schedule_profile_names & schedule_rules]) and a payload which can be used by the node-red-contrib-cron-plus node.

Oops edit: forgot to put a 0 in seconds of cron expression

[{"id":"c5980b7598462543","type":"debug","z":"f859c7434a67ede7","name":"Shelly Out","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":690,"y":160,"wires":[]},{"id":"9dfc059a6c2db66a","type":"inject","z":"f859c7434a67ede7","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"profile_name\":\"Living Room Weekdays\",\"rules\":[{\"days\":\"12345\",\"time\":\"10:30\",\"temperature\":18},{\"days\":\"12345\",\"time\":\"14:30\",\"temperature\":20},{\"days\":\"12345\",\"time\":\"22:30\",\"temperature\":15}]},{\"profile_name\":\"Living Room Weekend\",\"rules\":[{\"days\":\"06\",\"time\":\"11:30\",\"temperature\":18},{\"days\":\"06\",\"time\":\"14:30\",\"temperature\":20},{\"days\":\"06\",\"time\":\"22:30\",\"temperature\":15}]}]","payloadType":"json","x":190,"y":160,"wires":[["3c1cab6a40a0d9c4","5128e2c3c6f15a0c"]]},{"id":"3c1cab6a40a0d9c4","type":"function","z":"f859c7434a67ede7","name":"Shelly Msg","func":"const schedules = msg.payload\n\nlet shellySchedule = {\n        schedule_profile_names: [],\n        schedule_rules: []\n\n}\n\nschedules.forEach((schedule) => {\n    shellySchedule.schedule_profile_names.push(schedule.profile_name)\n    schedule.rules.forEach(rule => {\n        shellySchedule.schedule_rules.push(`${rule.time.replace(':', '')}-${rule.days}-${rule.temperature}`)\n        })  \n})\n\nmsg.shelly = shellySchedule\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":160,"wires":[["c5980b7598462543"]]},{"id":"5128e2c3c6f15a0c","type":"function","z":"f859c7434a67ede7","name":"CRON Msg","func":"function strToDays(string) {\n    const daysArray = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']\n    let days = ''\n\n    for (let i = 0; i < string.length; i++) {\n        days = days + ',' + daysArray[string.charAt(i)]\n\n    }\n\n    return days.slice(1)\n}\n\nconst schedules = msg.payload\nconst cron = ['0', '*', '*', '*', '*', '*']\nlet cronExpression = ''\nlet cronExpressions = []\nlet cronSchedule = []\n\nschedules.forEach((schedule) => {\n    schedule.rules.forEach((rule, index) => {\n        cron[5] = strToDays(rule.days)\n        cron[1] = rule.time.split(':')[1]\n        cron[2] = rule.time.split(':')[0]\n        cronExpression = cron.reduce((accumulator, currentValue) => accumulator + ' ' + currentValue)\n        cronExpressions.push(cronExpression)\n\n        cronSchedule.push({ command: 'add', \n                            name: schedule.profile_name + ' ' + index, \n                            topic: 'schedule/' + schedule.profile_name, \n                            expression: cronExpression,\n                            expressionType: 'cron',\n                            payloadType: 'default',\n                            payload: rule.temperature,\n                            limit: null })\n    })\n    \n})\n\nmsg.payload = cronSchedule\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":220,"wires":[["c5980b7598462543","0cef1eca98cb9705"]]},{"id":"e0f3f3d0e46771d5","type":"debug","z":"f859c7434a67ede7","name":"CRON Out","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1030,"y":220,"wires":[]},{"id":"eac62bbb6d723009","type":"inject","z":"f859c7434a67ede7","name":"List All to CRON Out","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"list-all","x":510,"y":280,"wires":[["0cef1eca98cb9705"]]},{"id":"0cef1eca98cb9705","type":"cronplus","z":"f859c7434a67ede7","name":"","outputField":"payload","timeZone":"","persistDynamic":true,"commandResponseMsgOutput":"output1","outputs":1,"options":[],"x":740,"y":220,"wires":[["e0f3f3d0e46771d5"]]}]

Having looked at the Shelly TRV data I do not quite see what the benefit of schedule_profile_names gives to controlling the TRV as there is no link to the schedule_rules but they make a handy way of adding information to data in Node-red

1 Like

@mudwalker,
Thanks a lot for sharing your solution!!!

Similar to the feedback from @Buckskin, I would prefer to have the schedule as json in my Inject nodes:

image

Don't have any other suggestions. Looks nice.

Just two questions:

  1. What is the purpose of the function node after the http request node?

  2. Am I correct that we use http instead of mqtt, only because we don't know how this works with mqtt? I am now wondering if http has disadvantages compared to mqtt? Just to know whether it is required that we spend more time on looking for an mqtt based solution...

1 Like

Do you mean to let the cron-plug node send commands to the TRV, at the moments where there are switchpoints in the profile. So that Node-RED tells the TRV when to go to another temperature?

If so then you need to measure very often the temperature, which means a lot of communication with the TRV. As a result the TRV battery will be empty much faster.

Therefore the idea was to send once a day the entire schedule to the TRV, and let the TRV itself decide when it needs to switch to another temperature.

But perhaps I misunderstood your proposal.

No. In @mudwalkers function, that you highlighted, he was creating a schedule for his boiler (at least that is what I think it was there for), so I created the cron output for that as that did seem to require a command for each schedule change.

My proposal (with the shelly output) is to send the schedule data to the TRV and let it get on with it. The idea suggested by @mudwalker of a schedule file (or context) was a good one so I wanted to come up with a way of converting my idea of the schedule into something that could be sent to a Shelly TRV.

I have a different TRV but I only have to send a schedule if it changes (and the schedule covers weekdays & weekends so it almost never gets changed). I do not think I have sent the TRVs a command since I set the attributes how I wanted them when I first installed them.

2 Likes

@Buckskin With JSON - I seem to hit a brick wall, or at least, get taken out of my comfort zone! I seem to ge ton well, and then have a horrendous crash, which knocks my confidence! BUT, your's and Bart's observation regarding JSON from the inject Node looks good. I will have to try it.

I did experiment with a CRON PLUS job as an 'active' driver to adjust temperature, but ended up 'developing my own' as I was not 100% sure about CRON after a power off. (I say my own, but it is not rocket science!! :rofl:) That is a solution that I know works, and can easily be included in a 'parameter refresh' loop but now I can see the logic behind your comments with regard to JSON. Don't like fetching files anyway if I can help it, always seems a bit clunky!

Agreed, the schedule_profile_names does not add anything other than convenience of identification.

@BartButenaers Thank you, always worried at what people will say with regard to code, but I also treat it as 'Training Opportunity' for someone to 'hedjucate' me!!

The function node you have indicated injects the current value for the desired temperature of the Living Room into context without accessing the TRV. This is then used by the rest of my flow to

  1. Switch the boiler ON/OFF
  2. Control the Diverter Valve

I used HTTP as I could not find the equivalent MQTT command string. iirc, all Shelly API calls for the TRV were developed for HTTP and MQTT has gradually caught up, but the documentation has been well behind the curve either incorrect (thermostat/s). Now, maybe, you can find something I missed, in which case, please let me know. (Would JSON work?)

I don't know that MQTT is better than HTTP from a power standpoint, I would assume yes, but don't know.


The idea is to load all the profiles onto the TRV and let that run autonomously, then be able to select the profile by changing the Profile Number. That then requires the Boiler to know what is going on, I pull the current profile into a function node (without communicating with the TRV) to strip out the current required temperature so that I can control the boiler and the Diverter Valve.

I will look at a way of using the cronplus node to load the profile schedules so that I can get the current data, but only send those schedules to the TRV if there is a change. Or, more likely, use the above, but instead of using a file, use JSON.

My concern is that after a Power Cut, the Boiler Control flow needs to hit the ground running with the correct values and maintain them as each step of the schedule changes and account for any change in profile. No good having a power cut for a couple of days and then Boiler operating in 'Night Mode' while the Radiator is running in 'Day Mode'.

Thanking you for your comments and observations, Any questions, ask away!

4 Likes

Hey guys,
To start with: a Merry Christmas :santa: :gift: :christmas_tree:

I am going to start with my TRV today, in a desperate attempt to give my body some time to digest the food and drinks from yesterday evening...

Don't think I understand the message structure correctly. In the API documentation Shelly gives this example:

    "schedule_profile": 2,
    "schedule_profile_names": [
        "Livingroom",
        "Livingroom 1",
        "Bedroom",
        "Bedroom 1",
        "Holiday"
    ],
    "schedule_rules": [
        "0600-0123456-23",
        "0830-0123456-18",
        "1630-0123456-23",
        "2300-0123456-18"
    ],

Question 1 - schedule_profile_names

The schedule_profiles_names is only used for visual identification in the web interface of the TRV if I understand correctly. Anyway would like to have it clear and self explaining, in case I need to do troubleshooting in the future...

The schedule rules of the above example specify that this profile (consisting out of 4 setpoints) should be run every day of the week (due to 0123456). But why on earth are here 5 different profile names?

I thought that if the schedule rules would contain N setpoint profiles, that you would have N setpoint profile names. For example a profile for the weekend (06) and one for the weekdays (12345):

    "schedule_profile": 2,
    "schedule_profile_names": [
        "Bathroom profile for the week",
        "Bathroom profile for the weekend"
    ],
    "schedule_rules": [
        # Setpoint profiles for the week
        "0700-12345-20",
        "0820-12345-19",
        "1730-12345-25",
        "2200-12345-17"
        # Setpoint profiles for the weekend
        "0600-06-23",
        "0830-06-18",
        "1630-06-23",
        "2300-06-18"
    ],

But seems NOT to be working like that. Of course a bit normal, because the Shelly doesn't know this way anyway which profile name would correspond to which profile setpoint values...

But instead their example gives profile names like "Bedroom" and "Living room". Don't understand why a TRV - which is installed in a single room - returns the profile names of multiple rooms...

Question 2 - schedule_profile

The schedule_profile allows you to "Choose an active schedule profile with the provided id 1..5". Again I don't think I understand this correctly. You can't have 7 profiles, in case you want a different setpoint profile every day. Which is of course again a bit normal I think, because the schedule_rules can contain the setpoints of multiple days (via 0123456).

My plan was to send via Node-RED every morning a schedule for today to the TRV. Am I correct that this is always schedule_profile "1", i.e. the profile of today? Not sure if I would need multiple different profiles.

Question 3 - schedule_rules

Via the schedule_rules I can set the temperature at a specific time of the day, according to the Shelly documentation:

I thought that I had read somewhere that this is the desired temperature at that time. So then the TRV valve will need to open earlier to reach my desired temperature in time. Not sure how he does that, because he doesn't know the outside temperature (which determines how much earlier he has to start heating my room).

Suppose this is indeed the desired temperature. Let's say I want to have my bathroom at 20 degrees (Celsius of course :wink: ) between 07:00 and 08:00:

image

I can easily specify a schedule rule "0700-0123456-20" to make sure it is warm when I enter the room at 07:00. But which schedule rule do I need to enter for 08:00? Do I have to repeat the first rule like "0800-0123456-20" to make sure he keeps the room warm until that time. But I don't know how fast the room cools down to 15 degrees. So should I say "0815-0123456-15", or "0820-0123456-15", or "0830-0123456-15", or ...

Would be nice if somebody could clarify a bit. Perhaps stupid questions, but I don't get it...

Yes, Merry Christmas to All!

My understanding of Profiles/Schedules:
There can be up to 5 Profiles numbered 1-5
You can select your desired Profile 1-5 with schedule_profile
schedule_profile_names only seems to be used by us Humans to aid easier identification
I have added 7 events to a profile with no problem. (Is there a limit?)

The examples I feel sure, are just examples, probably 'developed' by the software engineer to test different variables with names to suit their test (you know how ridiculous developers get when developing, everything for convenience to test a scenario, not for actual real world practicality).

My Living Room TRV is staying where it is and won't be used elsewhere, but I can select a profile to suit. Visitors? - a little warmer (if they are welcome, a little cooler if not! :rofl:)

I am going to try a profile that has events that occur on different days, say, at the weekend turn the heating off later, so the next to last event switches heating off at 21:30, but at the weekend, not until 22:30...

{
    "schedule_profile": 5,
    "profile_name": "Different off times",
    "rules": [
        {
            "days": "0123456",
            "time": "11:30",
            "temperature": 18
        },
        {
            "days": "01234",
            "time": "21:30",
            "temperature": 15
        },
        {
            "days": "56",
            "time": "22:30",
            "temperature": 15
        }
    ]
}

Not sure how the 'predictive' algorithm works, but agree, without outside temperature it is at best a guess. I do have a form of 'weather compensation'. At the moment, I change the temperature of the boiler to give faster response if colder, but this affects the efficiency. My next plan is to work on the time, so that I can predict when the boiler needs to come on to heat the room effectively, but need data on the boiler/house response/inertia

FWIW, I am now feeding the TRV schedules in using JSON.

I do not use the Shelly but I imagine that most TRVs are similar. The Shelly knows the room temperature

    "tmp": {
        "value": 17.4,
        "units": "C",
        "is_valid": true
    },

but I would think that, like most thermostats, there is no anticipation of scheduled temperatures so the valve only tries to adjust once a set time has arrived. From looking at the documentation I think

ext_t.enabled bool If temperature correction from external temp sensor is enabled

that you can use an external temperature sensor as a control if you wish.

The other issue to bear in mind is that the TRV has no idea of the current temperature of the water flowing in the radiator, so if the heating is not on it can be wide open, with no actual effect. :thinking:

Yes indeed that was what I was also thinking the first time I read that sentence. However afterwards I read somewhere that this can be used if you have a temperature sensor somewhere in your room, which you want to use instead of the temperature sensor inside the TRV. So it is to measure the temperature inside your room, not outdoors...

That would be indeed a useful experiment. My confusion is caused because it "feels" to me that Shelly is mixing setpoint profiles (for a single day) with schedules, by the days parameter...

But this can be manipulated in Node-red using both internal & external sensors because all the valve is after is a temperature, source is irrelevant.

I haven't though through how this can happen but what is required is a value that causes the valve to open or close dependant on the weighting given to each sensor.

So, for example, if the room temperature is as scheduled the only action is making sure that the temperature sent to the valve is above the scheduled temperature.
However, if the room temperature is below the scheduled temperature then some calculated value, dependent on how far below scheduled temperature it is and what impact you are allowing for the external temperature to have, should be send to the valve.

1 Like

Just a suggestion: did you consider to drop shelly an email with your findings instead ?

Yes indeed that might be an option. But I had hoped the Shellies could do this out of the box. They are not really cheap after all...

Talking about this, I think I have misinterpreted the "desired temperature" that you can set for a specific timestamp via a profile. Suppose I activate those schedule_rules:

"0000-0123456-15"
"0700-0123456-20"
"0800-0123456-15"

That the result will be:

I have not had time to install my TRV to test this, but if this is the case then the TRV is less smart as a thought. Which means a profile would be used to set temperatures on the TRV automatically, instead of having to press manually on the physical TRV up/down buttons.

Of course then you could indeed measure a temperature sensor via Node-RED, and adjust in the morning the profile before you send it to the TRV. When it is cold outside, you have to move your switchpoints foreward because it will take more time to heaten up the room:

image

The only problem I see is that your Node-RED flow needs to learn how the room's heating time depends on the outside temperature. And to measure that, you need to send frequently requests to the TRV to get the current temperature. And that will drain the TRV battery fast...

That might be a good idea indeed yes, as soon as I have got it up and running...

I put together a very over-the-top flow (based on something totallyInformation said) and get this graph for each of my TRV's. The data is sent by the TRV, and sensor, not requested, when they feel like it (about every 1/2 an hour). The position line is the actual open position of the valve (between 0 - 100%). I have only been running this for the last day (the graph covers 24hrs) so it is too early to tell if I am going to learn anything. (but it was fun doing it)

image

The only problem I see is that your Node-RED flow needs to learn how the room's heating time depends on the outside temperature.

This is a problem with any system that tries to anticipate a start time, not only does the TRV need to be trained but so does your boiler. If you do come up with something I bet there are bunches of people who would be interested. The only way I have come up with is to use a lookup table, but it would take a fair while to compile the data required.

And to measure that, you need to send frequently requests to the TRV to get the current temperature

Just use the temperature from a temperature sensor (I use Aqara zigbee units which seem fairly accurate - sensor in graph)

1 Like

Don't want to too much off topic here, but you might ask your question in this discussion.

ext_t.enabled is indeed a temperature sensor external to the Shelly TRV. I use a room sensor I have built from an ESP8266, but of course, as long as the Shelly can see the temperature on the Network, it doesn't care where it comes from.

The Profile/Schedule event shows what the desired temperature needs to be at a certain time. I think there may possibly be (distancing myself from this statement) some kind of predictive algorithm for 'thinking forward', but details are distinctly fuzzy as far as I can tell.

I am a strong believer that when you have several 'intelligent' items working together, unless they are designed to do so, it is better if they are made dumb and a Co-ordinator used to act as the intelligence. I have always found that intelligent thing are more likely to fight rather than assist each other with no proper integration.

I also feel from my experiments that there is enough inertia in the heating system to allow jut on/off operation, which I control with Node-RED. Currently, it doesn't seem to need a Deadband for the Living Room. This would hopefully mean that operation of the TRV uses the least amount of power, so extending lifetime.

Basically, I am heading along a path that (I hope) will use the minimum of interaction with the TRV (with maximum control).

  1. Load Qty 5 Profiles/Schedules with x events in each to suit what is needed.
  2. Schedules can be selected via Node-RED Dashboard.
  3. Use of local external temperature sensor (never liked measuring temperature at the point of a heat source, they cross talk too much!)
  4. Use Node-RED to also monitor same local temperature and 'drive' the boiler.

(I am not a believer in leaving boilers on all the time 'as it is cheaper'. Higher dwelling internal temperatures mean higher losses. But there is a minimum temperature above which a dwelling should be kept to enable a reasonable recovery time after no occupation.)

  1. Develop predictive routines to help attain profile desired.

(Currently I change Water Tank temperature and Boiler temperature depending upon Outside Temperature - just a straight y=mx + c straight line graph. More complex models can be developed once the 'inertia' of the dwelling is modelled - if you want to go that far.)

  1. Doing the heavy lifting in one common place over which you have control is easier than in several other places over which you have no proper control, unless those different systems are designed to operate with each other and you can observe what is going on.

Just my tthoughts.

1 Like

I see that they also offer an offset setting:

TEMPERATURE OFFSET
There are two options you can tick to activate - Enable external temperature corrections endpoint http://192.168.207.208/ext_t?temp=measured and Enable internal temperature compensation logic. The second feature helps to read the temperature correctly despite the proximity of the heat source. Below that, there is a texbox for you to enter the temperature offset you desire. The Shelly TRV measures the temperature of its immediate environment. For this reason, the Shelly TRV records a temperature slightly higher than the average temperature of your room. Input an offset value so that the Shelly TRV display the accurate, average room temperature.

Could this be useful perhaps?

It would be nice if you could share your updated flow, so I can "loot" some of your ideas :wink:

There is an article on auto-calibration of TRVs here Your TRV is lying to you: TRV auto-calibration - NotEnoughTech

While the TRVs and sensors used are zigbee the concept and ideas are still relevant and could be re-done for the Shellys with no bother.

I started looking at this because the TRVs used are the same as I use and there is a whole series of related articles.

1 Like

Sorry, I am following this thread but we have family visiting so computer time is limited.

Yes, I have seen the offset, but whichever way I do things, I am required to interrogate or update the TRV on a regular basis for the current temperature status. My idea is to only communicate with the TRV when I need to change something.

If I use the offset, I have to interrogate the TRV to update NR with the Current Temperature, or use the regular status message to see what the temperature is. If I use NR and the external source, I only have to communicate with the TRV when I want to change Open/Close and I hope to be able to reduce the sending of Status Message to conserve power.

Of course:

[{"id":"4f90b9a595b6f9b3","type":"inject","z":"29ddb158bef0fd2c","name":"Select current Profile","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[     {         \"schedule_profile\": 1,         \"profile_name\": \"Living Room\",         \"rules\": [             {                 \"days\": \"0123456\",                 \"time\": \"06:00\",                 \"temperature\": 19             },             {                 \"days\": \"0123456\",                 \"time\": \"08:00\",                 \"temperature\": 21             },             {                 \"days\": \"0123456\",                 \"time\": \"10:30\",                 \"temperature\": 19             },             {                 \"days\": \"0123456\",                 \"time\": \"15:00\",                 \"temperature\": 20             },             {                 \"days\": \"0123456\",                 \"time\": \"17:00\",                 \"temperature\": 21             },             {                 \"days\": \"0123456\",                 \"time\": \"18:00\",                 \"temperature\": 22             },             {                 \"days\": \"0123456\",                 \"time\": \"21:30\",                 \"temperature\": 16             }         ]     },     {         \"schedule_profile\": 2,         \"profile_name\": \"Living Room Weekend\",         \"rules\": [             {                 \"days\": \"06\",                 \"time\": \"07:00\",                 \"temperature\": 21             },             {                 \"days\": \"06\",                 \"time\": \"10:00\",                 \"temperature\": 20             },             {                 \"days\": \"06\",                 \"time\": \"16:00\",                 \"temperature\": 20             },             {                 \"days\": \"06\",                 \"time\": \"18:00\",                 \"temperature\": 22             },             {                 \"days\": \"06\",                 \"time\": \"22:00\",                 \"temperature\": 16             }         ]     },     {         \"schedule_profile\": 3,         \"profile_name\": \"AM Visitors\",         \"rules\": [             {                 \"days\": \"0123456\",                 \"time\": \"07:00\",                 \"temperature\": 21             },             {                 \"days\": \"0123456\",                 \"time\": \"13:00\",                 \"temperature\": 19             },             {                 \"days\": \"0123456\",                 \"time\": \"16:00\",                 \"temperature\": 20             },             {                 \"days\": \"0123456\",                 \"time\": \"18:00\",                 \"temperature\": 22             },             {                 \"days\": \"0123456\",                 \"time\": \"21:30\",                 \"temperature\": 16             }         ]     },     {         \"schedule_profile\": 4,         \"profile_name\": \"PM Visitors\",         \"rules\": [             {                 \"days\": \"0123456\",                 \"time\": \"07:00\",                 \"temperature\": 21             },             {                 \"days\": \"0123456\",                 \"time\": \"10:30\",                 \"temperature\": 19             },             {                 \"days\": \"0123456\",                 \"time\": \"12:00\",                 \"temperature\": 21             },             {                 \"days\": \"0123456\",                 \"time\": \"18:00\",                 \"temperature\": 22             },             {                 \"days\": \"0123456\",                 \"time\": \"21:30\",                 \"temperature\": 16             }         ]     },     {         \"schedule_profile\": 5,         \"profile_name\": \"Spare\",         \"rules\": [             {                 \"days\": \"06\",                 \"time\": \"11:30\",                 \"temperature\": 18             },             {                 \"days\": \"06\",                 \"time\": \"14:30\",                 \"temperature\": 20             },             {                 \"days\": \"06\",                 \"time\": \"22:30\",                 \"temperature\": 15             }         ]     }  ]","payloadType":"jsonata","x":410,"y":580,"wires":[["7af6b66a1c9f1fde"]]},{"id":"7af6b66a1c9f1fde","type":"function","z":"29ddb158bef0fd2c","name":"split file","func":"let profileSelected = (global.get('ZprofileSelect') - 1) || 0;\nmsg.payload = msg.payload[profileSelected];\nglobal.set('ZprofileEvents', msg.payload.rules);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":580,"y":580,"wires":[["6748bf65c6058e19","ca44a403f52e4783"]]},{"id":"e1a0b7a516aa8e84","type":"inject","z":"29ddb158bef0fd2c","name":"Profile 1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":180,"y":520,"wires":[["be2574bab2b971c1"]]},{"id":"4e777a15c7947d51","type":"inject","z":"29ddb158bef0fd2c","name":"Profile 2","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2","payloadType":"num","x":180,"y":560,"wires":[["be2574bab2b971c1"]]},{"id":"a33af1209e1b80e1","type":"inject","z":"29ddb158bef0fd2c","name":"Profile 3","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"3","payloadType":"num","x":180,"y":600,"wires":[["be2574bab2b971c1"]]},{"id":"4b9692153e429c8c","type":"inject","z":"29ddb158bef0fd2c","name":"Profile 4","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"4","payloadType":"num","x":180,"y":640,"wires":[["be2574bab2b971c1"]]},{"id":"5987db1c2ede4c6a","type":"inject","z":"29ddb158bef0fd2c","name":"Profile 5","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"5","payloadType":"num","x":180,"y":680,"wires":[["be2574bab2b971c1"]]},{"id":"be2574bab2b971c1","type":"change","z":"29ddb158bef0fd2c","name":"Set Profile","rules":[{"t":"set","p":"ZprofileSelect","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":520,"wires":[["ef9a5ec91786e7cb"]]},{"id":"ef9a5ec91786e7cb","type":"debug","z":"29ddb158bef0fd2c","name":"debug 123","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":510,"y":520,"wires":[]},{"id":"6748bf65c6058e19","type":"debug","z":"29ddb158bef0fd2c","name":"debug 124","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":710,"y":540,"wires":[]},{"id":"ca44a403f52e4783","type":"function","z":"29ddb158bef0fd2c","name":"set Profile time/deg/days","func":"let eventTime;\nlet eventTimeNC;\nlet eventTemp;\nlet eventDays;\nlet eventString = \"\";\nlet arr = msg.payload.rules;\nlet elements = arr.length;\n\nlet profileNumber = msg.payload.schedule_profile;\nlet profileName = msg.payload.profile_name;\n\nfor (let i = 0; i < elements; i++) {\n    eventTime = msg.payload.rules[i].time;\n    //Remove Colon, Shelly doesn't like\n    eventTimeNC = eventTime.replace(\":\", \"\")\n    eventTemp = msg.payload.rules[i].temperature;\n    eventDays = msg.payload.rules[i].days;\n    eventString = eventString + eventTimeNC + \"-\" + eventDays + \"-\" + eventTemp\n    if (i < elements-1){\n        eventString = eventString + \",\";\n    }\n}\n\nlet messageString = \"http://XXX.XXX.XXX.XXX/settings/thermostat/0?schedule_profile=\" + profileNumber + \"&profile_name=\" + profileName + \"&schedule_rules=\" + eventString\nmsg.url = messageString;\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":810,"y":580,"wires":[["64f33d3f8e5e561e","73185e4573535fba","5a17e1334ef54a25","3aa3ef0e2d9aa6b4"]]},{"id":"64f33d3f8e5e561e","type":"debug","z":"29ddb158bef0fd2c","name":"debug 125","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"url","targetType":"msg","statusVal":"","statusType":"auto","x":910,"y":540,"wires":[]},{"id":"1b7ae76f21b65952","type":"comment","z":"29ddb158bef0fd2c","name":"Select desired Profile (1-5)","info":"","x":370,"y":480,"wires":[]},{"id":"6b7544e1f9b0aa6c","type":"comment","z":"29ddb158bef0fd2c","name":"Assemble message for TRV from JSON","info":"","x":550,"y":620,"wires":[]},{"id":"73185e4573535fba","type":"function","z":"29ddb158bef0fd2c","name":"set CH time/deg/days","func":"let length;\nlet setHours;\nlet setMinutes;\nlet eventTime;\nlet eventDay;\nlet timeEvent;\nlet arr = global.get('ZprofileEvents');\n//CH background temp.\nlet tempDesired\nif (tempDesired === undefined) {\n    tempDesired = 16;\n}\n\n//Get current day start and end timestamp\nlet dateNow = Date.now();\nlet timeFrom = new Date(dateNow).setHours(0, 0, 0, 0);\nlet timeTo = new Date(dateNow).setHours(23, 59, 59, 0);\n\nconst d = new Date();\nlet day = ((d.getDay() + 7) - 1) % 7;\n\n//Add offset, iterate until file time < current time\nfor (let i = arr.length - 1; i >= 0; i--) {\n    eventTime = arr[i].time;\n    eventDay = arr[i].days\n    msg.test1 = eventTime;\n    setHours = eventTime.slice(0, 2);\n    setMinutes = eventTime.slice(3);\n    timeEvent = new Date(timeFrom).setHours(setHours, setMinutes, 0, 0); //Date from midnight this morning\n    tempDesired = parseFloat(arr[i].temperature);\n    msg.tempDesired = tempDesired;\n    msg.dateNow = dateNow;\n    msg.timeEvent = timeEvent;\n    msg.dayNow = day;\n    //msg.dayNow = eventDay.includes(day);\n\n\n    if (i === 0 && (dateNow < timeEvent)) {\n        tempDesired = parseFloat(arr[arr.length - 1].temperature);\n        global.set('ZtempDesiredCH',);\n        node.status({ text: timeEvent + \" | \" + tempDesired + \" | \" + \"<<<<<\" });\n        return msg\n    }\n    else if (dateNow > timeEvent && eventDay.includes(day)) {\n        global.set('ZtempDesiredCH', tempDesired, 'storeInFile');\n        node.status({ text: timeEvent + \" | \" + tempDesired });\n        return msg\n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":860,"y":660,"wires":[["d19584808941f204"]]},{"id":"d5cc96591e11e507","type":"debug","z":"29ddb158bef0fd2c","name":"debug 126","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1110,"y":540,"wires":[]},{"id":"dd6d14f02e5c9077","type":"inject","z":"29ddb158bef0fd2c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":620,"y":660,"wires":[["73185e4573535fba"]]},{"id":"5a17e1334ef54a25","type":"debug","z":"29ddb158bef0fd2c","name":"debug 127","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1090,"y":620,"wires":[]},{"id":"212226c1473cf92e","type":"comment","z":"29ddb158bef0fd2c","name":"CH tempDesired","info":"","x":480,"y":700,"wires":[]},{"id":"3aa3ef0e2d9aa6b4","type":"http request","z":"29ddb158bef0fd2c","name":"","method":"GET","ret":"txt","paytoqs":false,"url":"","persist":false,"insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":1090,"y":580,"wires":[["d5cc96591e11e507"]]},{"id":"d19584808941f204","type":"debug","z":"29ddb158bef0fd2c","name":"debug 128","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1070,"y":680,"wires":[]},{"id":"effb4dbd1604ac66","type":"comment","z":"29ddb158bef0fd2c","name":"using JSON","info":"","x":170,"y":480,"wires":[]}]

'Select desired profile' will allow changing selected profile by sending just a number.
'Assemble message for TRV' removes Colon from the time for the Shelly
'CHtempDesired' is the current temperature desired for the boiler control side of things (OTGW)

I will read @Buckskin link, hopefully today, but I have an inkling of what it is going to say. Solving problems with accurate temperature control was part of my role when I was working. This is the reason that I don't have much faith in temperature sensors being linked to the control unit right next to the heat source. Convenient, but not necessarily that accurate. Having said that, my expectations regarding temperature control are very high!

Keep asking and challenging, it makes me think!

Get this week out of the way and Family commitments drop off to allow more time to 'play'. I want to test the 'Combined Profile' for switching things on/off on different days at different times in the same Profile.

1 Like

Thinking about this at it's most basic level, I guess that I am really only needing a Dumb battery operated TRV with some form of wireless communication for the Main room. Being able to switch the valve Open/Closed would be adequate.

The positional control is something that I think is not required with the 'inertia' of the Main room.

However, a smaller/minor room, not hooked into NR, would benefit from the Profiles and positional control as it could monitor itself using the Profile. The temperature control is probably not as critical for comfort. If the radiator needs heat, but the Main room is up to temperature, then you would need some form of feedback to turn the boiler on. An update frequency of one minute would be more than adequate, even stretching to five or ten minutes, I can't see a problem. iirc In Tasmota, you can change the frequency/period at which status updates are sent, I will look for similar in Shelly. Need to investigate this.

BTW - Family responsibilities have reduced - we have the house to ourselves again.