Dutch local 'Stookwijzer'

In this subforum you can show projects you have made, or you are busy with. Please create your own topic.

Moderator: leecollings

janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Dutch local 'Stookwijzer'

Post by janpep »

I am willing to share my project in this topic which describes the setup I use for a local alert.
This is a different approach than the RIVM stookalert, which applies to the entire province.
Here it works with local information, with which the advice is calculated in the script.

All 6 items below are needed for it to work:
1. Custom sensor that holds the current local LKI value (Air quality index) based on your coordinates. I have set the axis-label is set to 'LKI'.
LuchtkwaliteitsIndex-JanPep.png
LuchtkwaliteitsIndex-JanPep.png (18.19 KiB) Viewed 4078 times

2. LKI script that will update the LKI sensor. The script runs every hour. Update of the LKI sensor will trigger the stoke-alert script to run.
You have to enter the device idx of the LKI sensor. Note that for logging, I make use of a function in global_data. You may use or modify this or comment it out.

Code: Select all

-- 05-01-2023: Script created by Jan peppink, https://ict.peppink.nl
-- Get data from https://api.luchtmeetnet.nl for LKI every hour for new values
-- Example url: url = 'https://api.luchtmeetnet.nl/open_api/concentrations?formula=lki&longitude=' .. long .. '&latitude=' .. lat .. '&start=' .. requestTime .. '&end=' .. requestTime
--              Use of Starttime and Endtime makes no difference

return {
	on = {
		timer = { 
		    'at *:02',  -- (LKI info online is updated at the hour)
        },
		httpResponses = {
		    'lki'       -- matches callback string below
        },
	},
	logging = {
		level = domoticz.LOG_INFO,
		marker = 'LKI-',
	},
	execute = function(domoticz, triggeredItem)
        -- Set Local environment=================
        -- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
        local debug_level = 0

        --local _u = domoticz.utils
        local _h = domoticz.helpers     -- Holds the global functions
        local _d = domoticz.globalData  -- Holds the global data

        -- Get location coordinates
        local lat = domoticz.settings.location.latitude
        local long = domoticz.settings.location.longitude
        
        -- currenTime = RawDateTime = 2022-12-06 19:50:00
        local currentTime = tostring(domoticz.time.rawDateTime)
        -- requestTime in IS08601 representation formatted amd for the current (whole) hour e.g. 2022-12-06T19:00:00Z
        local requestTime = domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'yyyy-mm-ddThh:00:00Z', 0 )
        -- resultTime comes back in json result e.g. 2022-12-06T19:00:00+00:00
        local resultTime = domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'yyyy-mm-ddThh:00:00+00:00', 0 )
        local nextresultTime = domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'yyyy-mm-ddThh:00:00+00:00', 3600 )

        local lki_idx = n --idx of the custom sensor 'Luchtkwaliteitsindex'
        local lkiValue = domoticz.devices(lki_idx).sensorValue  -- Gets last stored value
        
        local totallkiValues = 0    -- Will hold a count of the total of the values we get.
        local intloopCounter = 0    -- Will hold a count of the the number of the values we get.

        -- Now start to do something ============
        -- Remark: Starttime and endtime does not seem to do much.
		if (triggeredItem.isTimer) then
		    -- Log what we do.
		    _h.logthis( domoticz, debug_level, 'https://api.luchtmeetnet.nl/open_api/concentrations?formula=lki&longitude=' .. long .. '&latitude=' .. lat .. '&start=' .. requestTime .. '&end=' .. requestTime )
            -- Get the data.
			domoticz.openURL({
				url = 'https://api.luchtmeetnet.nl/open_api/concentrations?formula=lki&longitude=' .. long .. '&latitude=' .. lat .. '&start=' .. requestTime .. '&end=' .. requestTime,
				method = 'GET',
				callback = 'lki'
			})
		end	

	    if (triggeredItem.isHTTPResponse) then
	        -- Check the response and process the data
    		if (triggeredItem.ok and triggeredItem.isJSON) then
    		    _h.logthis( domoticz, debug_level, 'Item and JSON - OK' )
				-- We have some result. Store in table.
				local result_table = triggeredItem.json
                if type(result_table) == "table" then
    		        _h.logthis( domoticz, debug_level, 'resulttable: type = ' .. type(result_table) )
                    local data_table = result_table['data']
                    if type(data_table) == "table" then
                        _h.logthis( domoticz, debug_level, 'datatable: type = ' .. type(data_table) )
                        local tc = #data_table
				        for i = 1, tc do
                            -- Loop through the table. The data we get is:
				            -- "timestamp_measured": "2022-12-06T14:00:00+00:00"
				            -- formula": "LKI"
				            -- "value": 4.44999980926514
				            if data_table[i].timestamp_measured == resultTime then
				                _h.logthis( domoticz, debug_level, 'Current Recordtime: ' .. data_table[i].timestamp_measured )
				                --
                                lkiValue =  math.ceil( tonumber( data_table[i].value ) )
                                _h.logthis( domoticz, debug_level, 'Current Recordvalue: ' .. lkiValue )
                            end

                            -- The table can also contain values from past hours. Only count future hours
                            if data_table[i].timestamp_measured > resultTime  then
                                totallkiValues = totallkiValues + tonumber( data_table[i].value )
				                intloopCounter = intloopCounter + 1                                
                            end
                        end

                        -- Update device with lkiValue with new value (or the old-current).
                        domoticz.devices(lki_idx).updateCustomSensor(lkiValue) 
                        
                        -- Calculate and store the average value for future hours in global_data.
                        --NB. This will be used in script d-StookwijzerLokaal
                        _d.avgnextlkiValues = math.ceil( totallkiValues / intloopCounter )
                        -- Keep track of the number of hours that is received.
                        _d.avgnextlkiHours = intloopCounter
                        ---
                        _h.logthis( domoticz, debug_level, 'Totaal lki values = ' .. totallkiValues .. '; Number of records = ' .. _d.avgnextlkiHours .. '; avg = ' .. _d.avgnextlkiValues )
                    else
                        _h.logthis( domoticz, debug_level, 'No datatable found' )
                    end
                else
                    _h.logthis( domoticz, debug_level, 'No resulttable found' )
                end
			else
				_h.logthis( domoticz, debug_level, 'Item or JSON - NOT OK' )
			end
	    end
	end
}
3. Global_data set up where the LKI script stores the calculated avarage LKI for the coming hours in global_data, which will be picked up by the stoke-alert script. I give my entire 'global_data' script. You may or may not want to use the logging function. If not, you have also to modify corresponding lines in the scripts that call this function. Save this script as 'global_data', or take the used variables 'avgnextlkiValues' and 'avgnextlkiHours' in your existing script.

Code: Select all

-- 05-01-2023: Script created by Jan peppink, https://ict.peppink.nl
-- This scripts holds all the globally persistent variables and helper functions
-- See the documentation in the wiki
-- NOTE: THERE CAN BE ONLY ONE global_data SCRIPT in your Domoticz install.

return {
	-- Global persistent data
	data = {
        -- Used in t-Alarmeringen for Alarmeringen.nl.
		lastAlarmNotification = {},
        -- Used in t-Luchtkwaliteitindex + d-StookwijzerLokaal.
        avgnextlkiValues = { initial = 0 },
        avgnextlkiHours =  { initial = 0 },
    },
    -- Global helper functions
	helpers = {
        logthis = function( domoticz, debug_level, log_string )
		    -- Call this with domoticz.helpers.logthis( 'string to log'[, log_level])
	    	-- The function gives the option to log or not to log under certain conditions.
		    -- You then have only to set debug_level once in a script to change the logging level (or skip logging).
            debug_level = debug_level or 0
            if debug_level ~= 0 then
                if debug_level == 1 then
                    log_level = domoticz.LOG_INFO
                elseif debug_level == 2 then
                    log_level = domoticz.LOG_DEBUG
                elseif debug_level == 3 then
                    log_level = domoticz.LOG_ERROR
                elseif debug_level == 4
                    then log_level = domoticz.LOG_FORCE
                else 
                    return
                end
                -- Function. Creates a logging entry in the Domoticz log but respects the log level settings.
                -- domoticz.LOG_INFO, domoticz.LOG_DEBUG, domoticz.LOG_ERROR or domoticz.LOG_FORCE
                -- In Domoticz settings you can set the log level for dzVents.
                -- For optional log_level the default is LOG_INFO
                if log_string ~= "" then
                    -- domoticz.log(message, [level])
                    domoticz.log( log_string, log_level)
                else
                    return
                end
            end
        end
	}
}
4. Buienradar plugin (or another method with which you can get the current local windspeed). Like the LKI, Buienradar is based on your coordinates.
BuienradarWind-JanPep.png
BuienradarWind-JanPep.png (20.89 KiB) Viewed 4078 times

5. A stoke-alert script that combines the LKI, the stored global_data, the windspeed and winddirection and gives a calculated advice with some additional information. The calculation used is described on https://iplo.nl/thema/lucht/houtstook-s ... d2bfe71123
Or course you have to enter the idx numbers of the used devices.

Code: Select all

--[[
- 05-01-2023: Script created by Jan peppink, https://ict.peppink.nl
- Based on information at: https://iplo.nl/thema/lucht/houtstook-stookwijzer-stookalert/#h80dfe1e2-a855-424b-81b4-cdd2bfe71123
- Result: Update 'Lokale Stookwijzer', when 'Luchtkwaliteitsindex' is changed.
- Gives a treee line result with:
--  Alertcolor, Calculated code (in accordance with the definition on https://iplo.nl), current LKI.
--  Winddescription (Winddirection) windspeed in m/s, bft and km/h.
--  LKI expectation (average for number of coming hours that has been received.
- NB.   Expectation counts down when online information is not updatesd.
-       (remains future hours stored in file, until we pass the end of it)
- Depends on LKI device (= local Air Quality Index based on the coordinates).
- Depends on Buienradar 'BR-Wind' device. (Local windspeed based on Buienradar with given coordinates).
- Depends on stored average LKI for coming hours stored in global_data.
- Setting alertLevel by constant:  domoticz.ALERTLEVEL_GREY, ALERTLEVEL_GREEN, ALERTLEVEL_YELLOW, ALERTLEVEL_ORANGE, ALERTLEVEL_RED
- Getting alertLevel by number:    0=gray, 1=green, 2=yellow, 3=orange, 4=red
]]

--Set the device that triggers this script first.
local lki_idx = n -- idx of your 'Luchtkwaliteitsindex' device


return {
	on = {
		devices = {
			lki_idx,    -- idx of your 'Luchtkwaliteitsindex' device
		},
	},
	logging = {
        level = domoticz.LOG_INFO,
        marker = "LokaleStookwijzer-"
    },
	execute = function( domoticz )
        -- Set Local environment=================
        -- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
        local debug_level = 0

        --local _u = domoticz.utils
        local _h = domoticz.helpers     -- Holds the global functions
        local _d = domoticz.globalData  -- Holds the global data

        local lkiValue = domoticz.devices(lki_idx).sensorValue
        local alertIdx = n    -- 'Lokale Stookwijzer'
        local lkiText = '\nLKI verwachting komende ' .. _d.avgnextlkiHours .. ' uur is gemiddeld ' .. _d.avgnextlkiValues        

        local windSpeedIdx = n    -- Set to the idx of the (Buienradar) windspeed sensor.
        -- This one does not exist when Buienradar is off. SO default wind info to ...
        local windText = 'Lokale wind niet beschikbaar.'                                    

        if domoticz.devices(windSpeedIdx) ~= nil then 
            -- Buienradar WindSpeed does exist. Get the info.
            local windSpeedMs = domoticz.devices(windSpeedIdx).speedMs  -- Gets the current value
            local windDirection = domoticz.devices(windSpeedIdx).directionString    -- just for information.

            -- Translate abbreviation into Dutch abbreviation. E(ast) becomes O(ost) S(outh) becomes Z(uid).
            windDirection = string.gsub(windDirection, "E", "O",2)
            windDirection = string.gsub(windDirection, "S", "Z",2)

            -- Cannot remembeer from where I got these naming definitions.
            if windSpeedMs >= 0 and windSpeedMs <= 0.2 then windText = '\nWindstil (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 0 bft; < 1 km/u' end
            if windSpeedMs >= 0.3 and windSpeedMs <= 1.5 then windText = '\nZeer zwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 1 bft; 1-5km/u' end
            if windSpeedMs >= 1.6 and windSpeedMs <= 3.3 then windText = '\nZwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 2 bft; 6-11km/u' end
            if windSpeedMs >= 3.4 and windSpeedMs <= 5.4 then windText = '\nZeer matige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 3 bft; 12-19km/u' end
            if windSpeedMs >= 5.5 and windSpeedMs <= 7.9 then windText = '\nMatige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 4 bft; 20-28km/u' end
            if windSpeedMs >= 8.0 and windSpeedMs <= 10.7 then windText = '\nVrij krachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 5 bft; 29-38km/u' end
            if windSpeedMs >= 10.8 and windSpeedMs <= 13.8 then windText = '\nKrachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 6 bft; 39-49km/u' end
            if windSpeedMs >= 13.9 and windSpeedMs <= 17.1 then windText = '\nHarde wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 7 bft; 50-61km/u' end
            if windSpeedMs >= 17.2 and windSpeedMs <= 20.7 then windText = '\nStormachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 8 bft; 62-74km/u' end
            if windSpeedMs >= 20.8 and windSpeedMs <= 24.4 then windText = '\nStorm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 9 bft; 75-88km/u' end
            if windSpeedMs >= 24.5 and windSpeedMs <= 28.4 then windText = '\nZware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 10 bft; 89-102km/u' end
            if windSpeedMs >= 28.5 and windSpeedMs <= 32.6 then windText = '\nZeer zware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 11 bft; 103-117km/u' end
            if windSpeedMs > 32.6 then windText = '\nOrkaan (' .. windDirection .. ') ' .. windSpeedMs .. ' m/s = 12 bft; >117km/u' end

            -- Now start to do something ============
            --domoticz.log('Winds = ' .. windSpeedMs .. ' m/s and lkiValue = ' .. lkiValue)
            if _d.avgnextlkiHours == 0 and _d.avgnextlkiValues == 0 then
                --Code = Geel; No LKI data for current hour received.
                alertText = 'Niet beschikbaar!! Laatste LKI was '  .. lkiValue .. windText .. lkiText
                alertLevel = domoticz.ALERTLEVEL_YELLOW
                -- Update with new alertLevel and alertText.
                domoticz.devices(alertIdx).updateAlertSensor(alertLevel, alertText)
            elseif lkiValue <= 4 and windSpeedMs > 2 then
                --Code = Blauw; You can light the fireplace, but take the neighbors into account.
                --LKI <= 4 and windsspeed > 2m/sec.
                alertText = 'Code blauw LKI: '  .. lkiValue .. windText .. lkiText
                alertLevel = domoticz.ALERTLEVEL_GREEN
                -- Update with new alertLevel and alertText.
                domoticz.devices(alertIdx).updateAlertSensor(alertLevel, alertText)
            elseif lkiValue >= 5 and lkiValue <=7 and windSpeedMs > 2 then
                --Code = Oranje; It is better not to burn wood now.
                --LKI >= 5 and <= 7 and windspeed > 2m/sec.
                alertText = 'Code Oranje. LKI: '  .. lkiValue .. windText .. lkiText
                alertLevel = domoticz.ALERTLEVEL_ORANGE
                -- Update with new alertLevel and alertText.
                domoticz.devices(alertIdx).updateAlertSensor(alertLevel, alertText)
            elseif lkiValue >= 8 or windSpeedMs <= 2 then
                --Code = Rood; Do not burn wood.
                --LKI >= 8 and/or the windspeed is low (<= 2m/sec).
                alertText = 'Code Rood. LKI: '  .. lkiValue .. windText .. lkiText
                alertLevel = domoticz.ALERTLEVEL_RED
                -- Update with new alertLevel and alertText.
                domoticz.devices(alertIdx).updateAlertSensor(alertLevel, alertText)
            end
        else
            alertLevel = domoticz.ALERTLEVEL_YELLOW
            alertText = 'LKI: ' .. lkiValue .. " " .. windText .. lkiText
            domoticz.devices(alertIdx).updateAlertSensor(alertLevel, alertText)
        end
	end
}
6. An alert device, which holds the result. Levels are described in the script.
At the moment I am not satisfied with the orange alert, which comes up when an LKI >= 5, because the orange warning remains even when a strong wind is blowing. In that case I ignore the warning. So it may require some personal adjustment. At a low wind speed it always corresponds to my personal feeling.
LocaleStookwijzer-JanPep.png
LocaleStookwijzer-JanPep.png (18.73 KiB) Viewed 4078 times
Last edited by janpep on Saturday 23 March 2024 10:06, edited 2 times in total.
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
ajongen70
Posts: 13
Joined: Sunday 25 February 2024 0:28
Target OS: Raspberry Pi / ODroid
Domoticz version: 2024.3
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by ajongen70 »

Works like a charm! Thanks a lot for sharing!
BazemanKM
Posts: 35
Joined: Wednesday 22 July 2015 21:39
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Dutch local 'Stookwijzer'

Post by BazemanKM »

Hello,

I get an error:
2024-03-24 11:58:00.222 Error: dzVents: Error: (3.1.8) LKI-: An error occurred when calling event handler LuchtKwaliteitsIndex
2024-03-24 11:58:00.222 Error: dzVents: Error: (3.1.8) LKI-: ...ripts/dzVents/generated_scripts/LuchtKwaliteitsIndex.lua:50: attempt to index a nil value (local '_h')

I think it has to do something with the 'global_data' script. Where to place it, how to name it exactly?
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

BazemanKM wrote: Sunday 24 March 2024 12:00 I think it has to do something with the 'global_data' script. Where to place it, how to name it exactly?
Yes indeed. line 50 calls the logging function, so this does not seem to be available.

Steps for how and where to place and name the global_data:
  • Go to the internal editor and start a new script.
    global_data-JanPep.png
    global_data-JanPep.png (25.9 KiB) Viewed 3974 times
  • Replace the content of the template with the content of the global_data script I gave in the first post.
  • Name it 'global_data' and Save it.
That should do it.
Also see the information for the Shared helper functions in the DzVents wiki

And, as said, if you do not want to use the logging function, change it to what you like, comment it out or remove it.
I like it, because in this wayyou can put the logging code everywhere you want for testing. And you can set it on/off or adjust the level by changing only one variable in the beginning of the script.

Code: Select all

-- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
local debug_level = 0
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

A small addition:
Usually you see the average LKI forecast for the next 18 hours. but if the online file is not updated, you will see the counter decrease. The last saved forecast is then used. I really have no idea how reliable it is, but it's the best you have in that case. A few times I've experienced no updates for 18 hours. In that situation, a message will be shown and the script will use the last known values.
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
BazemanKM
Posts: 35
Joined: Wednesday 22 July 2015 21:39
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Re: Dutch local 'Stookwijzer'

Post by BazemanKM »

@janpep: Thanks, the global_data is new for me. Working great now.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

Of course, it makes no sense to implement an alert and then ignoring it. I am searching for the reasonable level of wind speed at which I can ignore the current orange warning code, when it appears due to a higher LKI. Then I can change the calculation for that. After all, my goal is mainly that I do not cause any inconvenience to my neighbors. This disappears when a strong wind blows.
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

I hereby share a newer version of the stoke alert script d-StookwijzerLokaal, which contains a few adjustments and additions.
Of course it makes sense to also trigger the script when there is a change in the wind speed. I used that before. In the trigger section I have replaced the idx of the windspeed device. Now commented out, because I don't use it anymore (explanation in the script). But you might want to try it out.

As mentioned earlier, I sometimes ignored the code orange when there was more wind. If the air quality is lower because for example pollution from Tata steel or other industry happens to blow in my direction, it doesn't really matter whether my little fireplace is burning or not, if a stronger wind blows. That's why I've finally made an addition that respects the definition and leaves it intact, but gives you the ability to make your own additional adjustments. I added two variables for this purpose:
- pepsOverridemode (0 or 1) to toggle on and off.
- acceptableWindspeed, which you can set to the desired limit that you find acceptable regardless of the LKI (between 5 and 7). I'm still experimenting with this and currently have it set to 5.5, which is between the definition of light and moderate wind.

Remember to set your own indexnumbers.

Code: Select all

--[[
- 05-01-2023: Script created by Jan peppink, https://ict.peppink.nl
- Based on information at: https://iplo.nl/thema/lucht/houtstook-stookwijzer-stookalert/#h80dfe1e2-a855-424b-81b4-cdd2bfe71123
- Result: Update 'Lokale Stookwijzer', when 'Luchtkwaliteitsindex' is changed.
- Gives a treee line result with:
--  Alertcolor, Calculated code (in accordance with the definition on https://iplo.nl), current LKI.
--  Winddescription (Winddirection) windspeed in m/s, bft and km/h.
--  LKI expectation (average for number of coming hours that has been received.
- NB.   Expectation counts down when online information is not updatesd.
-       (remains future hours stored in file, until we pass the end of it)
- Depends on LKI device (= local Air Quality Index based on the coordinates).
- Depends on Buienradar 'BR-Wind' device. (Local windspeed based on Buienradar with given coordinates).
- Depends on stored average LKI for coming hours stored in global_data.
- Setting alertLevel by constant:  domoticz.ALERTLEVEL_GREY, ALERTLEVEL_GREEN, ALERTLEVEL_YELLOW, ALERTLEVEL_ORANGE, ALERTLEVEL_RED
- Getting alertLevel by number: 0=gray, 1=green, 2=yellow, 3=orange, 4=red
- 25-03-2024 added two variables to override toggle if and when you want to override Orange warning definition. 
-   This is reasonable when LKI is 5, 6 or 7 and windspeed is acceptable.
-   Override is toggled by setting local pepsOverridemode = 1 (0 = off)
-   Acceptable windspeed can be set by local acceptableWindspeed = number
- I call it 'Code blauw-PLUS' - The alertcolor remains green.
]]

--Set the device that triggers this script first.
local lki_idx = n		-- idx of your 'Luchtkwaliteitsindex' device
local windSpeedIdx = n	-- idx of the (Buienradar) windspeed sensor.
--also set the 'target' device
local alertIdx = n		-- idx of the Local Stookwijzer sensor
----------------------- 
return {
	on = {
		devices = {
			lki_idx,	-- idx of your 'Luchtkwaliteitsindex' device
			--windSpeedIdx, -- Of course it makes sense to trigger this script also on change of the4 windspeed.
						-- then you can uncomment the idx of the windspeed here. I turned it off. 
						-- This is because there can be quite a few fluctuations in the wind speed,
						-- which can sometimes cause rather 'nervous' advice, which goes back and forth within the hour.
		},
	},
	logging = {
		level = domoticz.LOG_INFO,
		marker = "LokaleStookwijzer-"
	},
	execute = function( domoticz )
		-- Set Local environment=================
		-- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
		local debug_level = 0

		-- Set overrule for situation where LKI between 5 and 6, but windspeed > acceptable windspeed
		local pepsOverridemode = 1		-- 1 to enable the override. 0 to turn if off.
		local acceptableWindspeed = 5.5	-- Adjust the limit for the wind speed to be used in override mode.

		--local _u = domoticz.utils
		local _h = domoticz.helpers	 -- Holds the global functions
		local _d = domoticz.globalData  -- Holds the global data

		local lkiValue = domoticz.devices(lki_idx).sensorValue
		local lkiText = '\nLKI verwachting komende ' .. _d.avgnextlkiHours .. ' uur is gemiddeld ' .. _d.avgnextlkiValues

		-- It might happen that windSpeedIdx is not available (when Buienradar is off). SO default wind info to ...
		local windText = 'Lokale wind niet beschikbaar.'									

		if domoticz.devices(windSpeedIdx) ~= nil then 
			-- Buienradar WindSpeed does exist. Get the info.
			local windSpeedMs = domoticz.devices(windSpeedIdx).speedMs  -- Gets the current value
			local windDirection = domoticz.devices(windSpeedIdx).directionString	-- just for information.

			-- Translate abbreviation into Dutch abbreviation. E(ast) becomes O(ost) S(outh) becomes Z(uid).
			windDirection = string.gsub(windDirection, "E", "O",2)
			windDirection = string.gsub(windDirection, "S", "Z",2)

			-- Cannot remembeer from where I got these naming definitions.
			if windSpeedMs >= 0 and windSpeedMs <= 0.2 then windText = '\nWindstil (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 0 bft; < 1 km/u' end
			if windSpeedMs >= 0.3 and windSpeedMs <= 1.5 then windText = '\nZeer zwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 1 bft; 1-5km/u' end
			if windSpeedMs >= 1.6 and windSpeedMs <= 3.3 then windText = '\nZwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 2 bft; 6-11km/u' end
			if windSpeedMs >= 3.4 and windSpeedMs <= 5.4 then windText = '\nZeer matige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 3 bft; 12-19km/u' end
			if windSpeedMs >= 5.5 and windSpeedMs <= 7.9 then windText = '\nMatige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 4 bft; 20-28km/u' end
			if windSpeedMs >= 8.0 and windSpeedMs <= 10.7 then windText = '\nVrij krachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 5 bft; 29-38km/u' end
			if windSpeedMs >= 10.8 and windSpeedMs <= 13.8 then windText = '\nKrachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 6 bft; 39-49km/u' end
			if windSpeedMs >= 13.9 and windSpeedMs <= 17.1 then windText = '\nHarde wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 7 bft; 50-61km/u' end
			if windSpeedMs >= 17.2 and windSpeedMs <= 20.7 then windText = '\nStormachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 8 bft; 62-74km/u' end
			if windSpeedMs >= 20.8 and windSpeedMs <= 24.4 then windText = '\nStorm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 9 bft; 75-88km/u' end
			if windSpeedMs >= 24.5 and windSpeedMs <= 28.4 then windText = '\nZware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 10 bft; 89-102km/u' end
			if windSpeedMs >= 28.5 and windSpeedMs <= 32.6 then windText = '\nZeer zware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s = 11 bft; 103-117km/u' end
			if windSpeedMs > 32.6 then windText = '\nOrkaan (' .. windDirection .. ') ' .. windSpeedMs .. ' m/s = 12 bft; >117km/u' end

			-- Now start to do something ============
			--domoticz.log('Winds = ' .. windSpeedMs .. ' m/s and lkiValue = ' .. lkiValue)
			if _d.avgnextlkiHours == 0 and _d.avgnextlkiValues == 0 then
				--Code = Geel; No LKI data for current hour received.
				alertText = 'Niet beschikbaar!! Laatste LKI was '  .. lkiValue .. windText .. lkiText
				alertLevel = domoticz.ALERTLEVEL_YELLOW
			elseif lkiValue <= 4 and windSpeedMs > 2 then
				--Code = Blauw; You can light the fireplace, but take the neighbors into account.
				--LKI <= 4 and windsspeed > 2m/sec.
				alertText = 'Code blauw LKI: '  .. lkiValue .. windText .. lkiText
				alertLevel = domoticz.ALERTLEVEL_GREEN
			elseif lkiValue >= 5 and lkiValue <=7 and windSpeedMs > 2 then
				--Code = Oranje; It is better not to burn wood now.
				--LKI >= 5 and <= 7 and windspeed > 2m/sec.
				alertText = 'Code Oranje. LKI: '  .. lkiValue .. windText .. lkiText
				alertLevel = domoticz.ALERTLEVEL_ORANGE
			elseif lkiValue >= 8 or windSpeedMs <= 2 then
				--Code = Rood; Do not burn wood.
				--LKI >= 8 and/or the windspeed is low (<= 2m/sec).
				alertText = 'Code Rood. LKI: '  .. lkiValue .. windText .. lkiText
				alertLevel = domoticz.ALERTLEVEL_RED
			end
			
			-- You may want to override the code orange, when there is a certain wind speed.
			if pepsOverridemode == 1 and lkiValue >= 5 and lkiValue <=7 and windSpeedMs >= acceptableWindspeed then
				-- Added personal level Blauw-PLUS. LKI is higher, but ther is wind.
				--Code = Blauw; You can light the fireplace, but take the neighbors into account.
				alertText = 'Code blauw-PLUS LKI: '  .. lkiValue .. windText .. lkiText
				alertLevel = domoticz.ALERTLEVEL_GREEN
			end
			
			-- Now finally update with new alertLevel and alertText we have found.
			domoticz.devices(alertIdx).updateAlertSensor(alertLevel, alertText)
			
		else
			-- When we do not have a windspeed.
			alertLevel = domoticz.ALERTLEVEL_YELLOW
			alertText = 'LKI: ' .. lkiValue .. " " .. windText .. lkiText
			domoticz.devices(alertIdx).updateAlertSensor(alertLevel, alertText)
		end
	end
}
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

Expansion and improvements of the advice. Major changes in the scripts.

The reason for this was the fact that the wind speed forecast was missing and therefore no advice for the coming period could be calculated.

Finally I added and changed a lot:
  • All data from the same source (Open Meteo, Normal use is free for nun-commercial use without api-key.)
  • Local calculation of the LKI. (Open Meteo gives the components, but not the index as used in the calculation of the advice.)
  • Get also wind speed forcast for the same number of hours from Open Meteo.
  • Therefore a custom wind device shoud be created.
  • Having all the forcast data makes it possible to calculate the advice based on the average of the predicted LKI and wind speed.
  • The number of hours for the forecast is made adjustable between 1 and 72 via setpoint.
  • Therefore a custom setpoint device must also be created.
  • The maximum hour limit can also be set dynamically until 00:00 hour by a variable in the script.
  • Give color to the text of current advice and the forcast advice in accordance with the code.
  • Give up~ and downarrows with color to increasing or decresing values. Red is worse, green is better.
More extensive:
The forecast of only the air quality for the next few hours was of limited value. It doesn't mean much, if you have no idea how the wind speed will develop in that same time. With Buienradar I only get the current wind speed for my location.
I was looking for a wind forecast. I found this in the form of the free weather api from Open-Meteo. Free and without api key for non-commercial use and less than 10.000 daily API calls per day. (See other terms of use on their website).
There, also based on your own coordinates, data can be retrieved, including wind speed.
It can include both the current values and the expectation for several days.

Although there is a difference with the Buienradar, I decided to get the wind speed forecast from this source. I cannot explain the difference, nor determine which of the two is better. And then there is the fact that Buienradar does not give me future hours via the plug-in.
Now I am able to calculate the average future wind speed. Subsequently, the existing calculation on the average LKI and the average wind speed can be used to give a forecast recommendation.
To use the same source for the current and expected values, I have added a new custom wind device. The new script retrieves both current values and expected wind speed from open-meteo.

Then I decided to take everyting from the same source. Also the parameters for the air quality and calculate the current and forcast air quality index myself. This index can be calculated from Nitrogen dioxide, Ozone, PM10 and PM2,5 levels.

By using the average, you don't take into account intermediate fluctuations, but you do not turn your fireplace on and off every hour. So this average seemed to me to be an acceptable solution, which at least gives a better idea of the coming period.

To make it possible to easily change the number of hours for the advice, I created a setpoint device.
Changing the value triggers the script. You can set the limits of this device from 1 to 72 hour.

Then I realized that you normally check during the day to see if you can light the fireplace today. In that case it is sufficient to have advice that does not go beyond midnight. By setting a variable you can ensure that the setpoint is set dynamically until midnight. It counts down during the day. In this case it has no effect to change the setpoint manually.

The result looks like this:
LocaleStookwijzer+Setpoint-JanPep.png
LocaleStookwijzer+Setpoint-JanPep.png (27.92 KiB) Viewed 3862 times
I made the following adjustments to achieve this:
(Please note I sometimes mess around English and Dutch names a bit, but for the sake of convenience I will here stick to the names I use. Call it what you want. Also compared to the earlier scripts, I have renamed a few things.)

Steps to take:
First you need:
- Custom Setpoint device (with unit = hour, step = 1, min = 1, max = 72)
- Custom Sensor (with Ax-label LKI)
- Custom Alert device for the advice
- Custom Wind device (Wind+Temp+Chill is used!)
- Put the index numbers of your devices in the beginning of the scripts.

1.
I disabled the original t-Luchtkwaliteitindex script which took LKI from https://api.luchtmeetnet.nl

2.
I created a new more complex script dt-OpenMeteo which uses multiple callbacks.
It became a huge script, but it runs very fast. With testing the whole procedure with two api calls, all the callculations and updating the devices took about 700 ms.
- Script is triggered via the timer 'at *:02' or by changing the setpoint.
- The setpoint defines the forcastHourLimit if the script is not set to dynamic.
- At first start it gets the used air quallity parameters for the requested number of hours.
- API is called for 1, 2 or 3 days (data from 00:00 - 23:00). This gives you an absolute maximum of 72 hours.
- In the first callback it calculates the current LKI and the forcast LKI for the set number of hours.
- The LKI device is updated and the average of the coming hours is stored in a global_data variable.
- Then it calls (other api) for the wind data for the same number of hours.
- In the second callback it calculates the average wind speed prediction.
- The wind device is updated for the current hour and the average of the coming hours is stored in a global_data variable.
- When set to dynamic, the setpoint will be updated for the remaining number of hours of the day.
- This update of the setpoint will again trigger the script, but will be ignored and do nothing by break when set to dynamic.
- Update of the wind device in its turn triggers the script d-StokwijzerLokaal, which generates the advice.

Code: Select all

-- 26-03-2024: Script created by Jan peppink, https://ict.peppink.nl
-- Get data from https://open-meteo.com/
-- Free and without api key for non-commercial use and less than 10.000 daily API calls per day. (See other terms of use on their website).
-- Created URLs are visible in log when debug_level is set.
-- Script is triggered every hour on timer setting and also on change of the setpoint device (wanted #hours).
-- Setpoint can be set between 1 and 72 hour.
-- When you set useDynamicSetpoint = 1, the forcast is calculated until 00:00 hour and the setpoint dynamically counts down.
-- In ths case manually changing the Setpoint has no effect and will be overwritten next round.
-- Gets data in two rounds with multiple callbacks, because there is a separate api for weater(wind) and for airquality.
-- 1. Gets the current and forcast ozone, pm10, pm2_5, nitrogen_dioxide data for current location
--    Calculates LKI for current time and average LKI for the forcastHours.
--    Saves LKI value in LKI device and average in global_data
-- 2. Gets the current wind data and forecast wind speed for current location.
--    Saves wind values in wind device and average in global_data.
--    Updates setpoint device with current value, when useDynamicSetpoint = 1.
-- Works with _d.forcastHours, _d.avgForcastLKI and _d.avgForcastwindspeed from global_data


--- Your settings ----------------------------------------------------------------
-- First set the used device index numbers and variables you might want to change.
local meteolki_idx = n        -- idx of your custom sensor 'Luchtkwaliteitsindex'.
local setpoint_idx = n       -- idx of your custom setpoint device.
local meteowind_idx = n      -- idx of your custom meteowind sensor
local useDynamicSetpoint = 0    -- 1 = Limit forcast until 00:00 hour. 0 = Use Setpoint.

-- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
local debug_level = 0
        
----------------------------------------------------------------------------------
return {
	on = {
		timer = { 
			--'every 2 minutes',   -- Only used for testing.
            'at *:02',  -- (LKI info online is updated at the hour)
        },
		devices = {
			setpoint_idx,	-- idx of your setpoint for wanted forcastHours
		},
		httpResponses = {
            'meteoair',       -- matches callback string below
            'meteowind'       -- matches callback string below
        },
	},
	logging = {
		level = domoticz.LOG_INFO,
		marker = 'OpenMeteo-',
	},
	execute = function(domoticz, triggeredItem)
	    
        -- Set Local environment=================
        local _u = domoticz.utils       -- Holds subset of handy utilities.
        local _h = domoticz.helpers     -- Holds the global functions.
        local _d = domoticz.globalData  -- Holds the global data.

        -- Get location coordinates
        local lat = domoticz.settings.location.latitude
        local long = domoticz.settings.location.longitude

        -- currenTime = RawDateTime = 2022-12-06 19:50:00
        local currentTime = tostring(domoticz.time.rawDateTime)
        -- resultTime comes back in json result e.g. "2024-03-26T00:00"
        local resultTime = domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'yyyy-mm-ddThh:00', 0 )
        local nextHour = domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'hh', 3600 )

        -- For air and Wind data
        local pm10 = 0
        local pm2_5 = 0
        local nitrogen_dioxide = 0
        local ozone = 0
        local lki = 0
        local totallkiValues = 0    -- Will hold a count of the total of the values we get.
        local intloopCounter = 0    -- Will hold a count of the the number of the values we get.

        local meteowindSpeed = domoticz.devices(meteowind_idx).speedMs  -- Gets last stored value
        local totalwindspeedValues = 0    -- Will hold a count of the total of the values we get.

        -- For forcastHours calculation
        local forcastDays = 2 --2 is default
        local forcastHoursLimit = domoticz.devices(setpoint_idx).setPoint
        if useDynamicSetpoint == 1  then
            -- Calculate remaining hous untill (including) 00:00 o'clock.
            forcastHoursLimit = tonumber( 25 - nextHour )
        end
        if nextHour + forcastHoursLimit >= 48 then forcastDays = 3 end
        if nextHour + forcastHoursLimit < 48 then forcastDays = 2 end
        if nextHour + forcastHoursLimit < 24 then forcastDays = 1 end

        -- Local Functions go here =============
        --To calculate the LKI index.
        local function calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
            lki = 0
            --	                1	    2	    3	    4	    5	    6	    7	    8	    9	    10
            --ozone	            0-15	15-30	30-40	40-60	60-80	80-100	100-140	140-180	180-200	> 200
            --pm10              0-10	10–20	20–30	30-45	45-60	60-75	75-100	100-125	125-150	> 150
            --pm2_5             0-10	10-15	15-20	20-30	30-40	40-50	50-70	70-90	90-100	> 100
            --nitrogen_dioxide	0-10	10-20	20-30	30-45	45-60	60-75	75-100	100-125	125-150	> 150 
            if ozone > 0 and ozone <=15 then if lki < 1 then lki = 1 end end
            if pm10 > 0 and pm10 <= 10 then if lki < 1 then lki = 1 end end
            if pm2_5 > 0 and pm2_5 <= 10 then if lki < 1 then lki = 1 end end
            if nitrogen_dioxide  > 0 and nitrogen_dioxide <= 10 then if lki < 1 then lki = 1 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 1 = ' ..  lki )
            if ozone > 15 and ozone <= 30 then if lki < 2 then lki = 2 end end
            if pm10 > 10 and pm10 <= 20 then if lki < 2 then lki = 2 end end
            if pm2_5 > 10 and pm2_5 <= 15 then if lki < 2 then lki = 2 end end
            if nitrogen_dioxide > 10 and nitrogen_dioxide <= 20 then if lki < 2 then lki = 2 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 2 = ' ..  lki )
            if ozone > 30 and ozone <= 40 then if lki < 3 then lki = 3 end end
            if pm10 > 20 and pm10 <= 30 then if lki < 3 then lki = 3 end end
            if pm2_5 > 15 and pm2_5 <= 20 then if lki < 3 then lki = 3 end end
            if nitrogen_dioxide > 20 and nitrogen_dioxide <= 30 then if lki < 3 then lki = 3 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 3 = ' ..  lki )
            if ozone > 40 and ozone <= 60 then if lki < 4 then lki = 4 end end
            if pm10 > 30 and pm10 <= 45 then if lki < 4 then lki = 4 end end
            if pm2_5 > 20 and pm2_5 <= 30 then if lki < 4 then lki = 4 end end
            if nitrogen_dioxide > 30 and nitrogen_dioxide <= 45 then if lki < 4 then lki = 4 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 4 = ' ..  lki )
            if ozone > 60 and ozone <= 80 then if lki < 5 then lki = 5 end end
            if pm10 > 45 and pm10 <= 60 then if lki < 5 then lki = 5 end end
            if pm2_5 > 30 and pm2_5 <= 40 then if lki < 5 then lki = 5 end end
            if nitrogen_dioxide > 45 and nitrogen_dioxide <= 60 then if lki < 5 then lki = 5 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 5 = ' ..  lki )
            if ozone > 80 and ozone <= 100 then if lki < 6 then lki = 6 end end
            if pm10 > 60 and pm10 <= 75 then if lki < 6 then lki = 6 end end
            if pm2_5 > 40 and pm2_5 <= 50 then if lki < 6 then lki = 6 end end
            if nitrogen_dioxide > 60 and nitrogen_dioxide <= 75 then if lki < 6 then lki = 6 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 6 = ' ..  lki )
            if ozone > 100 and ozone <= 140 then if lki < 7 then lki = 7 end end
            if pm10 > 75 and pm10 <= 100 then if lki < 7 then lki = 7 end end
            if pm2_5 > 50 and pm2_5 <= 70 then if lki < 7 then lki = 7 end end
            if nitrogen_dioxide > 75 and nitrogen_dioxide <= 100 then if lki < 7 then lki = 7 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 7 = ' ..  lki )
            if ozone > 140 and ozone <= 180 then if lki < 8 then lki = 8 end end
            if pm10 > 100 and pm10 <= 125 then if lki < 8 then lki = 8 end end
            if pm2_5 > 70 and pm2_5 <= 90 then if lki < 8 then lki = 8 end end
            if nitrogen_dioxide > 100 and nitrogen_dioxide <= 125 then if lki < 8 then lki = 8 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 8 = ' ..  lki )
            if ozone > 180 and ozone <= 200 then if lki < 9 then lki = 9 end end
            if pm10 > 125 and pm10 <= 150 then if lki < 9 then lki = 9 end end
            if pm2_5 > 90 and pm2_5 <= 100 then if lki < 9 then lki = 9 end end
            if nitrogen_dioxide > 125 and nitrogen_dioxide <= 150 then if lki < 9 then lki = 9 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 9 = ' ..  lki )
            if ozone > 200 then lki = 10 end
            if pm10 > 150 then lki = 10 end
            if pm2_5 > 100 then lki = 10 end
            if nitrogen_dioxide > 150 then lki = 10 end
            --_h.logthis( domoticz, debug_level, 'LKI stap 10 = ' ..  lki )
            return lki
        end

        -- To calculate the windDirection.
        local function getWindDirectionString( windDirection )
            local directionString = ''
            -- When string is not in Enlish, the icon does not appear!
            if windDirection >= 0 and windDirection < 11.25 then directionString = 'N' end
            if windDirection >= 11.25 and windDirection < 33.75 then directionString = 'NNE' end
            if windDirection >= 33.75 and windDirection < 56.25 then directionString = 'NE' end
            if windDirection >= 56.25 and windDirection < 78.75 then directionString = 'ENE' end
            if windDirection >= 78.75 and windDirection < 101.25 then directionString = 'E' end
            if windDirection >= 101.25 and windDirection < 123.75 then directionString = 'ESE' end
            if windDirection >= 123.75 and windDirection < 146.25 then directionString = 'SE' end
            if windDirection >= 146.25 and windDirection < 168.75 then directionString = 'SSE' end
            if windDirection >= 168.75 and windDirection < 191.25 then directionString = 'S' end
            if windDirection >= 191.25 and windDirection < 213.75 then directionString = 'SSW' end
            if windDirection >= 213.75 and windDirection < 236.25 then directionString = 'SW' end
            if windDirection >= 236.25 and windDirection < 258.75 then directionString = 'WSW' end
            if windDirection >= 258.75 and windDirection < 281.25 then directionString = 'W' end
            if windDirection >= 281.25 and windDirection < 303.75 then directionString = 'WW' end
            if windDirection >= 303.75 and windDirection < 326.25 then directionString = 'NW' end
            if windDirection >= 326.25 and windDirection < 348.75  then directionString = 'NNW' end
            if windDirection >= 348.75 and windDirection <= 360 then directionString = 'N' end
            return directionString
        end

        -- Now start to do something ===========================
        if ( triggeredItem.isDevice ) then
            if useDynamicSetpoint == 1 then
                _h.logthis( domoticz, debug_level, 'useDynamicSetpoint is ON. No trigger for change of ' ..  domoticz.devices(setpoint_idx).name .. '.')
                -- Do not respond to change of the setPoint.
                return
            end
        end

        -- Get the data for air.
		if ( triggeredItem.isTimer or useDynamicSetpoint == 0 ) then
            -- Log what we do.
            _h.logthis( domoticz, debug_level, '#forcastDays to retreive is set to  ' .. forcastDays )
            _h.logthis( domoticz, debug_level, 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' .. lat .. '&longitude=' .. long .. '&current=pm10,pm2_5,nitrogen_dioxide,ozone&hourly=pm10,pm2_5,nitrogen_dioxide,ozone&timezone=auto&forecast_days=' .. forcastDays)
            -- Retrieve the data
            domoticz.openURL({
                url = 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' .. lat .. '&longitude=' .. long .. '&current=pm10,pm2_5,nitrogen_dioxide,ozone&hourly=pm10,pm2_5,nitrogen_dioxide,ozone&timezone=auto&forecast_days=' .. forcastDays,
                method = 'GET',
                callback = 'meteoair'
            })
		end	

        -- Process the obtained data.
        if ( triggeredItem.isHTTPResponse ) then
            -- For AIR ------------------------------------------
            if triggeredItem.trigger == 'meteoair' then
                -- Check the response and process the data.
                if ( triggeredItem.ok and triggeredItem.isJSON ) then
                    _h.logthis( domoticz, debug_level, 'Item and JSON - OK' )
                    -- We have some result. Store in table.
                    local result_table = triggeredItem.json
                    if type( result_table ) == "table" then
                        _h.logthis( domoticz, debug_level, 'result_table: type = ' .. type( result_table ) )
                        -- Now loop simultaneously trhough the hourly tables for the forcast
                        local tc = #result_table.hourly.time
                        for i = 1, tc do
                            -- Only take the future windspeed for the number of hours we have also for the LKI in _d.forcastHours.
                            if intloopCounter < 1 and result_table.hourly.time[i] == resultTime then
                                -- This is for the current hour.
                                pm10 = result_table.hourly.pm10[i]
                                pm2_5 = result_table.hourly.pm2_5[i]
                                nitrogen_dioxide = result_table.hourly.nitrogen_dioxide[i]
                                ozone = result_table.hourly.ozone[i]
                                lki = calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                _h.logthis( domoticz, debug_level, 'Current time = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - LKI = ' ..lki  .. '; pm10 = ' ..  pm10 .. '; pm2_5 = ' ..  pm2_5 .. '; nitrogen_dioxide = ' ..  nitrogen_dioxide ..'; ozone = ' ..  ozone )
                                -- Update sensor with current LKI value
                                _h.logthis( domoticz, debug_level, 'Update ' .. domoticz.devices(meteolki_idx).name .. '.' )
                                domoticz.devices(meteolki_idx).updateCustomSensor(lki)
                                intloopCounter = intloopCounter + 1
                            end
                            if result_table.hourly.time[i] > resultTime and intloopCounter <= forcastHoursLimit then
                                -- This is for the forcast
                                pm10 = result_table.hourly.pm10[i]
                                pm2_5 = result_table.hourly.pm2_5[i]
                                nitrogen_dioxide = result_table.hourly.nitrogen_dioxide[i]
                                ozone = result_table.hourly.ozone[i]
                                lki = calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                -- Update totallkiValues with current LKI value
                                totallkiValues =  totallkiValues + calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                _h.logthis( domoticz, debug_level, 'Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - LKI = ' ..lki  .. '; pm10 = ' ..  pm10 .. '; pm2_5 = ' ..  pm2_5 .. '; nitrogen_dioxide = ' ..  nitrogen_dioxide ..'; ozone = ' ..  ozone )
                                intloopCounter = intloopCounter + 1
                            end
                        end
                        -- Calculate and store the average value for future hours in global_data.
                        --NB. This will be used in script d-StookwijzerLokaal
                        -- Correct intloopCounter for last round
                        intloopCounter = intloopCounter -1
                        _d.avgForcastLKI = _u.round( totallkiValues / intloopCounter, 0 )
                        _h.logthis( domoticz, debug_level, 'Found average LKI = ' .. _d.avgForcastLKI .. ' in ' .. intloopCounter .. ' records.')
                        -- Keep track of the number of hours that is received.
                        _d.forcastHours = intloopCounter
                    else
                        _h.logthis( domoticz, debug_level, 'No result_table found' )
                    end
    			else
    				_h.logthis( domoticz, debug_level, 'Item or JSON - NOT OK' )
    			end

                -- Get the data for Wind.
                -- Log what we do.
                _h.logthis( domoticz, debug_level, 'forcastDays is set to  ' .. forcastDays )
                _h.logthis( domoticz, debug_level, 'https://api.open-meteo.com/v1/forecast?latitude=' .. lat .. '&longitude=' .. long .. '&current=temperature_2m,apparent_temperature,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=wind_speed_10m&wind_speed_unit=ms&timezone=auto&forecast_days=' .. forcastDays )
                -- Retrieve the data
                domoticz.openURL({
                    url = 'https://api.open-meteo.com/v1/forecast?latitude=' .. lat .. '&longitude=' .. long .. '&current=temperature_2m,apparent_temperature,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=wind_speed_10m&wind_speed_unit=ms&timezone=auto&forecast_days=' .. forcastDays,
                    method = 'GET',
				    callback = 'meteowind'
                })
            end -- meteoair

            -- For WIND -----------------------------------------
            if triggeredItem.trigger == 'meteowind' then
                -- Check the response and process the data.
                if (triggeredItem.ok and triggeredItem.isJSON) then
                    _h.logthis( domoticz, debug_level, 'Item and JSON - OK' )
                    -- We have some result. Store in table.
                    local result_table = triggeredItem.json
                    if type(result_table) == "table" then
                        _h.logthis( domoticz, debug_level, 'result_table: type = ' .. type(result_table) )
                        -- Get the current values
                        --"temperature_2m": 10.2,
                        --"apparent_temperature": 6.2,
                        --"wind_speed_10m": 4.71,
                        --"wind_direction_10m": 197,
                        --"wind_gusts_10m": 16.5
                        local temperature = result_table.current.temperature_2m
                        local chill = result_table.current.apparent_temperature
                        local windSpeedMs = _u.round( result_table.current.wind_speed_10m, 1 )
                        local windDirection = result_table.current.wind_direction_10m
                        local windDirectionString = getWindDirectionString( windDirection )
                        local windGust = result_table.current.wind_gusts_10m
                        _h.logthis( domoticz, debug_level, 'Current values - temperature = ' ..  temperature .. '; chill = ' ..  chill .. '; windSpeedMs = ' ..  windSpeedMs ..'; windDirection = ' ..  windDirection .. '; windDirectionString = ' ..  windDirectionString ..'; windGust = ' ..  windGust )

                        intloopCounter = 1
                        local tc = #result_table.hourly.time
                            _h.logthis( domoticz, debug_level, 'Forcasthours = ' .. _d.forcastHours )
                            for i = 1, tc do
                                if result_table.hourly.time[i] > resultTime and intloopCounter <= _d.forcastHours then
                                    _h.logthis( domoticz, debug_level, 'Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - ' .. result_table.hourly.wind_speed_10m[i])
                                    -- Value for the current hour.
                                    meteowindSpeed = result_table.hourly.wind_speed_10m[i]
                                    totalwindspeedValues = totalwindspeedValues + meteowindSpeed                          
                                    intloopCounter = intloopCounter + 1
                                end
                            end
                        -- Calculate and store the average value for future hours in global_data.
                        --NB. This will be used in script d-StookwijzerLokaal.
                        -- Correct intloopCounter for last round
                        intloopCounter = intloopCounter -1                    
                        _d.avgForcastwindspeed = _u.round( totalwindspeedValues / intloopCounter, 1 )
                        _h.logthis( domoticz, debug_level, 'Found average windspeed = ' .. _d.avgForcastwindspeed .. ' in ' .. intloopCounter .. ' records.')
                        
                        -- Now update our meteowinddevice, which triggers d-StookwijzerLokaal script to produce the advice.
                        _h.logthis( domoticz, debug_level, 'Update ' .. domoticz.devices(meteowind_idx).name  .. '.' )
                        domoticz.devices(meteowind_idx).updateWind( windDirection, windDirectionString, windSpeedMs, windGust, temperature, chill )                    
                        
                        --Finally update the setpoint if dynamic mode is enabled.
                        if useDynamicSetpoint == 1 then
                            -- Update the setpoint dynamically.
                            _h.logthis( domoticz, debug_level, 'useDynamicSetpoint is ON. Update ' ..  domoticz.devices(setpoint_idx).name .. ' to ' .. forcastHoursLimit .. '.' )
                            domoticz.devices(setpoint_idx).updateSetPoint( forcastHoursLimit )
                        end
                    else
                        _h.logthis( domoticz, debug_level, 'No result_table found' )
                    end
    			else
    				_h.logthis( domoticz, debug_level, 'Item or JSON - NOT OK' )
    			end
            end
        end
	end
}
-- That's All --------------------------------------------------
3.
I modified global_data
The average LKI and wind speed are stored here. They are called from scripts dt-OpenMeteo and d-Stokwijzerlokaal.
The logging function is also included in this central location, which is used from multiple scripts.
(Change or comment out the logging if it is not desired)

Code: Select all

-- 05-01-2023: Script created by Jan peppink, https://ict.peppink.nl
-- This scripts holds all the globally persistent variables and helper functions
-- See the documentation in the wiki
-- NOTE: THERE CAN BE ONLY ONE global_data SCRIPT in your Domoticz install.
-- 26-03-2024: renamed variables; added: avgForcastwindspeed
return {

	-- Global persistent data
	data = {
        -- Used in t-Alarmeringen for Alarmeringen.nl.
		lastAlarmNotification = {},
        -- Used in t-OpenMeteo-Air, d-OpenMeteo-Wind and d-StookwijzerLokaal.
        avgForcastLKI = { initial = 0 },
        avgForcastwindspeed = { initial = 0 },
        forcastHours = { initial = 0 },
    },

    -- Global helper functions
	helpers = {
        logthis = function( domoticz, debug_level, log_string )
		    -- Call this function with domoticz.helpers.logthis( domoticz, debug_level, 'string to log')
	    	-- The function gives the option to log or not to log under certain conditions.
		    -- You then have only to set debug_level once in a script to change the logging level (or skip logging).
            debug_level = debug_level or 0
            if debug_level ~= 0 then
                if debug_level == 1 then
                    log_level = domoticz.LOG_INFO
                elseif debug_level == 2 then
                    log_level = domoticz.LOG_DEBUG
                elseif debug_level == 3 then
                    log_level = domoticz.LOG_ERROR
                elseif debug_level == 4
                    then log_level = domoticz.LOG_FORCE
                else 
                    return
                end
                -- Function. Creates a logging entry in the Domoticz log but respects the log level settings.
                -- domoticz.LOG_INFO, domoticz.LOG_DEBUG, domoticz.LOG_ERROR or domoticz.LOG_FORCE
                -- In Domoticz settings you can set the log level for dzVents.
                -- For optional log_level the default is LOG_INFO
                if log_string ~= "" then
                    -- domoticz.log(message, [level])
                    domoticz.log( log_string, log_level )
                else
                    return
                end
            end
        end
	}
}
-- That's All --------------------------------------------------
4.
Many modifications to d-StookwijzerLokaal
- Gets LKI and Wind speed from the custom devices to calculate an advice for current hour.
- Gets average LKI and average wind speed from the global_data variables to calculate forcast advice.
- Compose the text advices with color as described before.
- You may also want to be able to overrule the code orange (LKI = 5-7) advice, when there is enough wind. I added two variables for this purpose:
- pepsOverridemode (0 or 1) to toggle on and off.
- acceptableWindspeed, which you can set to the desired treshold that you find acceptable regardless of the LKI (between 5 and 7). I'm still experimenting with this and currently have it set to 5.5, which is between the definition of light and moderate wind.
- To make the advice a bit clearer and to limit the text, I have added the html color of the advice. (In this case, it's a pity that there is no blue alert icon to choose. I want to keep the color codes of the original definition. I thing they wanted to avoid giving it a green light :). )
- Up and down arrows indicate the future development of LKI and wind speed. Color of the arrows (red or green) indicate whether this better or worse). And the color of the text indicates which code applies to the prediction.

Code: Select all

-- 05-01-2023: Script created by Jan peppink, https://ict.peppink.nl
-- Based on information at: https://iplo.nl/thema/lucht/houtstook-stookwijzer-stookalert/#h80dfe1e2-a855-424b-81b4-cdd2bfe71123
-- Gives a treee line result with:
--  Alertcolor, Calculated code (in accordance with the definition on https://iplo.nl), current LKI.
--  Winddescription (Winddirection) windspeed in m/s, bft and km/h.
--  LKI expectation (average for number of coming hours that has been received.
-- NB. Expectation hours counts down when online information is not updatesd.
--     (remains future hours stored online, until we pass the end of it)
-- Depends on LKI device (= local Air Quality Index based on the coordinates).
-- Depends on wind device. (Local windspeed based on given coordinates).
-- Depends on stored average LKI for coming hours stored in global_data.
-- Getting alertLevel by number: 0=gray, 1=green, 2=yellow, 3=orange, 4=red
-- Setting alertLevel by constant:  domoticz.ALERTLEVEL_GREY, ALERTLEVEL_GREEN, ALERTLEVEL_YELLOW, ALERTLEVEL_ORANGE, ALERTLEVEL_RED
-- 25-03-2024 added two variables to override toggle if and when you want to override Orange warning definition. 
--  This is reasonable when LKI is 5, 6 or 7 and windspeed is acceptable.
--  Override is toggled by setting local pepsOverridemode = 1 (0 = off)
--  Acceptable windspeed can be set by local acceptableWindspeed = number
--  I call it 'Code blauw-PLUS' - The alertcolor remains green.
-- 26-03-2024 Improved advice based on windspeed forcast.
--  Depends on stored average Windspeed for coming hours stored in global_data.
--  Calculates forcast advice based on avg LKI and avg wind speed in coming hours.

--- Your settings ----------------------------------------------------------------
-- First set the used device index numbers and variables you might want to change.
local meteowind_idx = n   --idx of your custom meteowind sensor, which triggers this script.
local meteolki_idx = n         -- idx of your 'Luchtkwaliteitsindex' device
local alert_idx = n        -- idx of the Local Stookwijzer sensor to store the advice.

-- Set overrule for situation where LKI between 5 and 6, but windspeed > acceptable windspeed
local pepsOverridemode = 1		-- 1 to enable the override. 0 to turn if off.
local acceptableWindspeed = 5.5	-- Adjust the limit for the wind speed to be used in override mode.

-- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
local debug_level = 0
        
----------------------------------------------------------------------------------
return {
	on = {
		devices = {
			meteowind_idx,  -- Update of meteowind, after update of LKI, triggers this script to run.
		},
	},
	logging = {
		level = domoticz.LOG_INFO,
		marker = "LokaleStookwijzer-"
	},
	execute = function( domoticz )
		-- Set Local environment=================
        --local _u = domoticz.utils       -- Holds subset of handy utilities.
		local _h = domoticz.helpers	 -- Holds the global functions
		local _d = domoticz.globalData  -- Holds the global data

		-- Get current LKI value
		local lkiValue = domoticz.devices(meteolki_idx).sensorValue
		
        -- Local Functions go here =============


		-- Now start to do something ============
		local windSpeedMs = domoticz.devices(meteowind_idx).speedMs  -- Gets the current value needed for calculation
		local windDirection = domoticz.devices(meteowind_idx).directionString	-- just for information.

		-- Translate abbreviation into Dutch abbreviation. E(ast) becomes O(ost) S(outh) becomes Z(uid).
		windDirection = string.gsub(windDirection, "E", "O",2)
		windDirection = string.gsub(windDirection, "S", "Z",2)

		-- Cannot remember from where I got these naming definitions.
		if windSpeedMs >= 0 and windSpeedMs <= 0.2 then windText = '\nWindstil (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596;0 bft &#8596; < 1 km/u' end
		if windSpeedMs >= 0.3 and windSpeedMs <= 1.5 then windText = '\nZeer zwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 1 bft&#8596; 1-5 km/u' end
		if windSpeedMs >= 1.6 and windSpeedMs <= 3.3 then windText = '\nZwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 2 bft &#8596; 6-11 km/u' end
		if windSpeedMs >= 3.4 and windSpeedMs <= 5.4 then windText = '\nZeer matige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 3 bft &#8596; 12-19 km/u' end
		if windSpeedMs >= 5.5 and windSpeedMs <= 7.9 then windText = '\nMatige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 4 bft &#8596; 20-28 km/u' end
		if windSpeedMs >= 8.0 and windSpeedMs <= 10.7 then windText = '\nVrij krachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 5 bft &#8596; 29-38 km/u' end
		if windSpeedMs >= 10.8 and windSpeedMs <= 13.8 then windText = '\nKrachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 6 bft &#8596; 39-49 km/u' end
		if windSpeedMs >= 13.9 and windSpeedMs <= 17.1 then windText = '\nHarde wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 7 bft &#8596; 50-61 km/u' end
		if windSpeedMs >= 17.2 and windSpeedMs <= 20.7 then windText = '\nStormachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 8 bft &#8596; 62-74 km/u' end
		if windSpeedMs >= 20.8 and windSpeedMs <= 24.4 then windText = '\nStorm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 9 bft &#8596; 75-88 km/u' end
		if windSpeedMs >= 24.5 and windSpeedMs <= 28.4 then windText = '\nZware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 10 bft &#8596; 89-102 km/u' end
		if windSpeedMs >= 28.5 and windSpeedMs <= 32.6 then windText = '\nZeer zware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 11 bft &#8596; 103-117 km/u' end
		if windSpeedMs > 32.6 then windText = '\nOrkaan (' .. windDirection .. ') ' .. windSpeedMs .. ' m/s &#8596; 12 bft &#8596; >117 km/u' end

        -- Prepare the forecast prefix sign
		-- Set the sign for expected increase or decrease. arrowup = &#8593; arrowdown = &#8595; equal = =
        local lkiSign = '<strong><span style="color: #CC00CC;">&#8596;</span></strong>'
		if _d.avgForcastLKI > lkiValue then
		      lkiSign = '<strong><span style="color: #ff0000;">&#9650;</span></strong>'
		elseif _d.avgForcastLKI < lkiValue then
		      lkiSign = '<strong><span style="color: #228B22;">&#9660;</span></strong>'
		end
        local windSign = '<strong><span style="color: #CC00CC;">&#8596;</span></strong>'
		if _d.avgForcastwindspeed > windSpeedMs then
		      windSign =  '<strong><span style="color: #228B22;">&#9650;</span></strong>'
		elseif  _d.avgForcastwindspeed < windSpeedMs then
		      windSign = '<strong><span style="color: #ff0000;">&#9660;</span></strong>'
		end
            
        -- Calculate and set the Current code.
		if _d.forcastHours == 0 and _d.avgForcastLKI == 0 then
			--Code = Geel; No LKI data for current hour received.
			alertText = 'Niet beschikbaar!! Laatste LKI was '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_YELLOW
		elseif lkiValue <= 4 and windSpeedMs > 2 then
			--Code = Blauw; You can light the fireplace, but take the neighbors into account.
			--LKI <= 4 and windsspeed > 2m/sec.
			alertText = '<span style="color: #0000FF;">Code blauw.</span> LKI: '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_GREEN
		elseif lkiValue >= 5 and lkiValue <=7 and windSpeedMs > 2 then
			--Code = Oranje; It is better not to burn wood now.
			--LKI >= 5 and <= 7 and windspeed > 2m/sec.
			alertText = '<span style="color: #FFA500;">Code oranje.</span> LKI: '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_ORANGE
		elseif lkiValue >= 8 or windSpeedMs <= 2 then
			--Code = Rood; Do not burn wood.
			--LKI >= 8 and/or the windspeed is low (<= 2m/sec).
			alertText = '<span style="color: #ff0000;">Code rood.</span> LKI: '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_RED
		end

        -- Set the forcast color
        local htmlColor = '#000000;'    --Black
        if _d.avgForcastLKI <= 4 and _d.avgForcastwindspeed > 2 then
            htmlColor = '#0000FF;'      -- Blue
        elseif _d.avgForcastLKI >= 5 and _d.avgForcastLKI <= 7 and _d.avgForcastwindspeed > 2 then
            htmlColor = '#FFA500;'      -- Orange
        elseif _d.avgForcastLKI >= 8 or _d.avgForcastwindspeed <= 2 then
            htmlColor = '#ff0000;'       -- Red
        end

		-- You may want to override the code orange, when there is a certain wind speed.
		if pepsOverridemode == 1 then
		    -- For current advice.
		    if lkiValue >= 5 and lkiValue <=7 and windSpeedMs >= acceptableWindspeed then
			    -- Added personal level Blauw-PLUS. LKI is higher, but there is wind.
			    --Code = Blauw; You can light the fireplace, but take the neighbors into account.
			    alertText = '<span style="color: #0000FF;">Code blauw-PLUS.</span> LKI: '  .. lkiValue .. windText
			    alertLevel = domoticz.ALERTLEVEL_GREEN
            end
            -- For the forecast advice color.
            if  _d.avgForcastLKI >= 5 and _d.avgForcastLKI <= 7 and _d.avgForcastwindspeed >= acceptableWindspeed then
			    htmlColor = 'blue'
			end
		end

		-- Generate the new forecast.
        alertText = alertText .. '\n<span style="color: ' .. htmlColor .. '"> x&#772; verwachting ' .. _d.forcastHours .. ' uur </span>: LKI' .. lkiSign .. _d.avgForcastLKI .. ' en wind' .. windSign .. _d.avgForcastwindspeed .. ' m/s'			

		-- Now finally update with new alertLevel and alertText we have found.
        _h.logthis( domoticz, debug_level, 'Update ' .. domoticz.devices(alert_idx).name  .. '.' )
		domoticz.devices(alert_idx).updateAlertSensor(alertLevel, alertText)
	end
}
-- That's All --------------------------------------------------
Result with dynamic setpoint (Time =- 21:02 hr.)
Note the code blauw-PLUS, which is caused by setting pepsOverridemode =1.
Normally LKI of 5 would give code Oranje. Now with Wind > 5.5 m/s it becomes blauw-Plus.
LocaleStookwijzer+Setpoint2-JanPep.png
LocaleStookwijzer+Setpoint2-JanPep.png (28.59 KiB) Viewed 3861 times

### That's all ###
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

I found a bug, that leads into an unwanted and almost endless loop when useDynamicSetpoint == 0
You will especially notice this if the setpoint is increased, because then the number of loops increases.
See these lines:

Code: Select all

        -- Get the data for air.
	if ( triggeredItem.isTimer or useDynamicSetpoint == 0 ) then
This causes it to retrieve the air data every time it reaches this point and useDynamicSetpoint == 0
I changed this in:

Code: Select all

        -- Get the data for air.
	if ( triggeredItem.isTimer or ( triggeredItem.isDevice and useDynamicSetpoint == 0 ) ) then
Data for air is then only retrieved if the script is triggerd by the timer, or the setpoint device (and the dynamic mode is not activated)

I have some more changes to both scripts and I will pupblish these later. It needs some testing.
- a bug for 23 hour when useDynamicSetpoint == 1.
- Color code changed. See: Informatiepunt Leefomgeving
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

Actually the 'Dutch' in the title of this topic no longer applies with the use of Open Meteo.

Changed scripts.
For dt-OpenMeteo:
- Fixed a problem when useDynamicSetpoint is used. At 23 hr. nextHour is not 24 but 00. Therefore the forcastHoursLimit was not set correctly.
- Fixed bug as mentioned before that caused a loop when useDynamicSetpoint == 0.
- I added a link to the LKI calculation definition in the script.

Code: Select all

-- 26-03-2024: Script created by Jan peppink, https://ict.peppink.nl
-- Get data from https://open-meteo.com/
-- Free and without api key for non-commercial use and less than 10.000 daily API calls per day. (See other terms of use on their website).
-- Created URLs are visible in log when debug_level is set.
-- Script is triggered every hour on timer setting and also on change of the setpoint device (wanted #hours).
-- Setpoint can be set between 1 and 72 hour.
-- When you set useDynamicSetpoint = 1, the forcast is calculated until 00:00 hour and the setpoint dynamically counts down.
-- In ths case manually changing the Setpoint has no effect and will be overwritten next round.
-- Gets data in two rounds with multiple callbacks, because there is a separate api for weater(wind) and for airquality.
-- 1. Gets the current and forcast ozone, pm10, pm2_5, nitrogen_dioxide data for current location
--    Calculates LKI for current time and average LKI for the forcastHours.
--    Saves LKI value in LKI device and average in global_data
-- 2. Gets the current wind data and forecast wind speed for current location.
--    Saves wind values in wind device and average in global_data.
--    Updates setpoint device with current value, when useDynamicSetpoint = 1.
-- Works with _d.forcastHours, _d.avgForcastLKI and _d.avgForcastwindspeed from global_data
-- Added option to useDynamicSetpoint until midnight. This overrules manually set Setpoint and also ignores device trigger.
-- 07-04-2024: Changed forcastHours calculation. Fix for 23 hr. when useDynamicSetpoint = 1 to set forcastHoursLimit = 1.
-- 10-04-2024: Fixed bug that caused a loop when triggered on device and useDynamicSetpoint == 0.

--- Your settings ----------------------------------------------------------------
-- First set the used device index numbers and variables you might want to change.
local meteolki_idx = n        -- idx of your custom sensor 'Luchtkwaliteitsindex'.
local setpoint_idx = n       -- idx of your custom setpoint device.
local meteowind_idx = n      -- idx of your custom meteowind sensor
local useDynamicSetpoint = 1    -- 1 = Limit forcast until 00:00 hour. 0 = Use Setpoint.

-- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
local debug_level = 0
        
----------------------------------------------------------------------------------
return {
	on = {
		timer = { 
			--'every 2 minutes',    -- Only used for testing.
            'at *:02',              -- (LKI info online is updated at the hour)
        },
		devices = {
			setpoint_idx,	-- idx of your setpoint for wanted forcastHours
                            -- Will be ignored by a break when useDynamicSetpoint = 1
		},
		httpResponses = {
            'meteoair',       -- matches callback string below
            'meteowind'       -- matches callback string below
        },
	},
	logging = {
		level = domoticz.LOG_INFO,
		marker = 'OpenMeteo-',
	},
	execute = function(domoticz, triggeredItem)
	    
        -- Set Local environment=================
        local _u = domoticz.utils       -- Holds subset of handy utilities.
        local _h = domoticz.helpers     -- Holds the global functions.
        local _d = domoticz.globalData  -- Holds the global data.

        -- Get location coordinates
        local lat = domoticz.settings.location.latitude
        local long = domoticz.settings.location.longitude

        -- currenTime = RawDateTime = e.g. 2022-12-06 19:50:00
        local currentTime = tostring( domoticz.time.rawDateTime )
        -- resultTime comes back in json result e.g. "2024-03-26T00:00"
        local resultTime = domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'yyyy-mm-ddThh:00', 0 )
        local nextHour = domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'hh', 3600 )

        -- For air and Wind data
        local pm10 = 0
        local pm2_5 = 0
        local nitrogen_dioxide = 0
        local ozone = 0
        local lki = 0
        local totallkiValues = 0    -- Will hold a count of the total of the values we get.
        local intloopCounter = 0    -- Will hold a count of the the number of the values we get.

        local meteowindSpeed = domoticz.devices( meteowind_idx ).speedMs  -- Gets last stored value.
        local totalwindspeedValues = 0    -- Will hold a count of the total of the values we get.

        -- For forcastHours calculation
        local forcastDays = 2 --2 is default
        local forcastHoursLimit = _u.round( domoticz.devices( setpoint_idx ).setPoint, 0 )
        _h.logthis( domoticz, debug_level, 'Setpoint = ' .. forcastHoursLimit )
        if useDynamicSetpoint == 1  then
            -- Calculate remaining hous untill (including) 00:00 o'clock, which is next day.
            _h.logthis( domoticz, debug_level, 'currentTime = ' .. currentTime .. ' and nextHour = ' .. nextHour)
            if domoticz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'hh', 0 ) == '23' then
                -- Exeption! nexthour '00' does not fit with forcastDays calculation below.
                -- Cannot add '00' hr. Therefore set it nextHour to 24, which is 1 hour from now.
                nextHour = 24
                forcastHoursLimit = 1                
                _h.logthis( domoticz, debug_level, 'For dynamic setpoint calculation correct nextHour to ' .. nextHour .. ' and set forcatHoursLimit to '  ..forcastHoursLimit )
            else
                forcastHoursLimit = _u.round( 25 - nextHour, 0 )
            end
        end
        if nextHour + forcastHoursLimit >= 48 then forcastDays = 3 end
        if nextHour + forcastHoursLimit < 48 then forcastDays = 2 end
        if nextHour + forcastHoursLimit < 24 then forcastDays = 1 end

        -- Local Functions go here =============
        local function calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
            --To calculate the LKI index. See https://www.rivm.nl/bibliotheek/rapporten/2014-0050.pdf
            -- The component that falls into the highest category determines the value of the index.
            lki = 0
            --INDEX             1	    2	    3	    4	    5	    6	    7	    8	    9	    10
            --ozone	            0-15	15-30	30-40	40-60	60-80	80-100	100-140	140-180	180-200	> 200
            --pm10              0-10	10–20	20–30	30-45	45-60	60-75	75-100	100-125	125-150	> 150
            --pm2_5             0-10	10-15	15-20	20-30	30-40	40-50	50-70	70-90	90-100	> 100
            --nitrogen_dioxide	0-10	10-20	20-30	30-45	45-60	60-75	75-100	100-125	125-150	> 150 
            if ozone > 0 and ozone <=15 then if lki < 1 then lki = 1 end end
            if pm10 > 0 and pm10 <= 10 then if lki < 1 then lki = 1 end end
            if pm2_5 > 0 and pm2_5 <= 10 then if lki < 1 then lki = 1 end end
            if nitrogen_dioxide  > 0 and nitrogen_dioxide <= 10 then if lki < 1 then lki = 1 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 1 = ' ..  lki )
            if ozone > 15 and ozone <= 30 then if lki < 2 then lki = 2 end end
            if pm10 > 10 and pm10 <= 20 then if lki < 2 then lki = 2 end end
            if pm2_5 > 10 and pm2_5 <= 15 then if lki < 2 then lki = 2 end end
            if nitrogen_dioxide > 10 and nitrogen_dioxide <= 20 then if lki < 2 then lki = 2 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 2 = ' ..  lki )
            if ozone > 30 and ozone <= 40 then if lki < 3 then lki = 3 end end
            if pm10 > 20 and pm10 <= 30 then if lki < 3 then lki = 3 end end
            if pm2_5 > 15 and pm2_5 <= 20 then if lki < 3 then lki = 3 end end
            if nitrogen_dioxide > 20 and nitrogen_dioxide <= 30 then if lki < 3 then lki = 3 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 3 = ' ..  lki )
            if ozone > 40 and ozone <= 60 then if lki < 4 then lki = 4 end end
            if pm10 > 30 and pm10 <= 45 then if lki < 4 then lki = 4 end end
            if pm2_5 > 20 and pm2_5 <= 30 then if lki < 4 then lki = 4 end end
            if nitrogen_dioxide > 30 and nitrogen_dioxide <= 45 then if lki < 4 then lki = 4 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 4 = ' ..  lki )
            if ozone > 60 and ozone <= 80 then if lki < 5 then lki = 5 end end
            if pm10 > 45 and pm10 <= 60 then if lki < 5 then lki = 5 end end
            if pm2_5 > 30 and pm2_5 <= 40 then if lki < 5 then lki = 5 end end
            if nitrogen_dioxide > 45 and nitrogen_dioxide <= 60 then if lki < 5 then lki = 5 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 5 = ' ..  lki )
            if ozone > 80 and ozone <= 100 then if lki < 6 then lki = 6 end end
            if pm10 > 60 and pm10 <= 75 then if lki < 6 then lki = 6 end end
            if pm2_5 > 40 and pm2_5 <= 50 then if lki < 6 then lki = 6 end end
            if nitrogen_dioxide > 60 and nitrogen_dioxide <= 75 then if lki < 6 then lki = 6 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 6 = ' ..  lki )
            if ozone > 100 and ozone <= 140 then if lki < 7 then lki = 7 end end
            if pm10 > 75 and pm10 <= 100 then if lki < 7 then lki = 7 end end
            if pm2_5 > 50 and pm2_5 <= 70 then if lki < 7 then lki = 7 end end
            if nitrogen_dioxide > 75 and nitrogen_dioxide <= 100 then if lki < 7 then lki = 7 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 7 = ' ..  lki )
            if ozone > 140 and ozone <= 180 then if lki < 8 then lki = 8 end end
            if pm10 > 100 and pm10 <= 125 then if lki < 8 then lki = 8 end end
            if pm2_5 > 70 and pm2_5 <= 90 then if lki < 8 then lki = 8 end end
            if nitrogen_dioxide > 100 and nitrogen_dioxide <= 125 then if lki < 8 then lki = 8 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 8 = ' ..  lki )
            if ozone > 180 and ozone <= 200 then if lki < 9 then lki = 9 end end
            if pm10 > 125 and pm10 <= 150 then if lki < 9 then lki = 9 end end
            if pm2_5 > 90 and pm2_5 <= 100 then if lki < 9 then lki = 9 end end
            if nitrogen_dioxide > 125 and nitrogen_dioxide <= 150 then if lki < 9 then lki = 9 end end
            --_h.logthis( domoticz, debug_level, 'LKI stap 9 = ' ..  lki )
            if ozone > 200 then lki = 10 end
            if pm10 > 150 then lki = 10 end
            if pm2_5 > 100 then lki = 10 end
            if nitrogen_dioxide > 150 then lki = 10 end
            --_h.logthis( domoticz, debug_level, 'LKI stap 10 = ' ..  lki )
            return lki
        end

        local function getWindDirectionString( windDirection )
            -- To calculate the windDirection.
            local directionString = ''
            -- When string is not in Enlish, the icon does not appear!
            if windDirection >= 0 and windDirection < 11.25 then directionString = 'N' end
            if windDirection >= 11.25 and windDirection < 33.75 then directionString = 'NNE' end
            if windDirection >= 33.75 and windDirection < 56.25 then directionString = 'NE' end
            if windDirection >= 56.25 and windDirection < 78.75 then directionString = 'ENE' end
            if windDirection >= 78.75 and windDirection < 101.25 then directionString = 'E' end
            if windDirection >= 101.25 and windDirection < 123.75 then directionString = 'ESE' end
            if windDirection >= 123.75 and windDirection < 146.25 then directionString = 'SE' end
            if windDirection >= 146.25 and windDirection < 168.75 then directionString = 'SSE' end
            if windDirection >= 168.75 and windDirection < 191.25 then directionString = 'S' end
            if windDirection >= 191.25 and windDirection < 213.75 then directionString = 'SSW' end
            if windDirection >= 213.75 and windDirection < 236.25 then directionString = 'SW' end
            if windDirection >= 236.25 and windDirection < 258.75 then directionString = 'WSW' end
            if windDirection >= 258.75 and windDirection < 281.25 then directionString = 'W' end
            if windDirection >= 281.25 and windDirection < 303.75 then directionString = 'WW' end
            if windDirection >= 303.75 and windDirection < 326.25 then directionString = 'NW' end
            if windDirection >= 326.25 and windDirection < 348.75  then directionString = 'NNW' end
            if windDirection >= 348.75 and windDirection <= 360 then directionString = 'N' end
            return directionString
        end

        -- Now start to do something ===========================
        if ( triggeredItem.isDevice ) then
            if useDynamicSetpoint == 1 then
                _h.logthis( domoticz, debug_level, 'useDynamicSetpoint is ON. No trigger for change of ' ..  domoticz.devices(setpoint_idx).name .. '.')
                -- Do not respond to change of the setPoint.
                return
            end
        end

        -- Get the data for air.
		if ( triggeredItem.isTimer or ( triggeredItem.isDevice and useDynamicSetpoint == 0 ) ) then
            -- Log what we do.
            _h.logthis( domoticz, debug_level, '#forcastDays to retreive is set to  ' .. forcastDays )
            _h.logthis( domoticz, debug_level, 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' .. lat .. '&longitude=' .. long .. '&current=pm10,pm2_5,nitrogen_dioxide,ozone&hourly=pm10,pm2_5,nitrogen_dioxide,ozone&timezone=auto&forecast_days=' .. forcastDays)
            -- Retrieve the data
            domoticz.openURL({
                url = 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' .. lat .. '&longitude=' .. long .. '&current=pm10,pm2_5,nitrogen_dioxide,ozone&hourly=pm10,pm2_5,nitrogen_dioxide,ozone&timezone=auto&forecast_days=' .. forcastDays,
                method = 'GET',
                callback = 'meteoair'
            })
		end	

        -- Process the obtained data.
        if ( triggeredItem.isHTTPResponse ) then
            -- For AIR ------------------------------------------
            if triggeredItem.trigger == 'meteoair' then
                -- Check the response and process the data.
                if ( triggeredItem.ok and triggeredItem.isJSON ) then
                    _h.logthis( domoticz, debug_level, 'Item and JSON - OK' )
                    -- We have some result. Store in table.
                    local result_table = triggeredItem.json
                    if type( result_table ) == "table" then
                        _h.logthis( domoticz, debug_level, 'result_table: type = ' .. type( result_table ) )
                        -- Now loop simultaneously trhough the hourly tables for the forcast
                        local tc = #result_table.hourly.time
                        for i = 1, tc do
                            -- Only take the future windspeed for the number of hours we have also for the LKI in _d.forcastHours.
                            if intloopCounter < 1 and result_table.hourly.time[i] == resultTime then
                                -- This is for the current hour.
                                pm10 = result_table.hourly.pm10[i]
                                pm2_5 = result_table.hourly.pm2_5[i]
                                nitrogen_dioxide = result_table.hourly.nitrogen_dioxide[i]
                                ozone = result_table.hourly.ozone[i]
                                lki = calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                _h.logthis( domoticz, debug_level, 'CurrentTime Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - LKI = ' ..lki  .. '; pm10 = ' ..  pm10 .. '; pm2_5 = ' ..  pm2_5 .. '; nitrogen_dioxide = ' ..  nitrogen_dioxide ..'; ozone = ' ..  ozone )
                                -- Update sensor with current LKI value
                                _h.logthis( domoticz, debug_level, 'Update ' .. domoticz.devices( meteolki_idx ).name .. '.' )
                                domoticz.devices( meteolki_idx ).updateCustomSensor( lki )
                                intloopCounter = intloopCounter + 1
                            end
                            if result_table.hourly.time[i] > resultTime and intloopCounter <= forcastHoursLimit then
                                -- This is for the forcast
                                pm10 = result_table.hourly.pm10[i]
                                pm2_5 = result_table.hourly.pm2_5[i]
                                nitrogen_dioxide = result_table.hourly.nitrogen_dioxide[i]
                                ozone = result_table.hourly.ozone[i]
                                lki = calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                -- Update totallkiValues with current LKI value
                                totallkiValues =  totallkiValues + calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                _h.logthis( domoticz, debug_level, 'Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - LKI = ' ..lki  .. '; pm10 = ' ..  pm10 .. '; pm2_5 = ' ..  pm2_5 .. '; nitrogen_dioxide = ' ..  nitrogen_dioxide ..'; ozone = ' ..  ozone )
                                intloopCounter = intloopCounter + 1
                            end
                        end
                        -- Calculate and store the average value for future hours in global_data.
                        --NB. This will be used in script d-StookwijzerLokaal
                        -- Correct intloopCounter for last round
                        intloopCounter = intloopCounter -1
                        _d.avgForcastLKI = _u.round( totallkiValues / intloopCounter, 0 )
                        _h.logthis( domoticz, debug_level, 'Found average LKI = ' .. _d.avgForcastLKI .. ' in ' .. intloopCounter .. ' records.' )
                        -- Keep track of the number of hours that is received.
                        _d.forcastHours = intloopCounter
                    else
                        _h.logthis( domoticz, debug_level, 'No result_table found' )
                    end
    			else
    				_h.logthis( domoticz, debug_level, 'Item or JSON - NOT OK' )
    			end

                -- Get the data for Wind.
                -- Log what we do.
                _h.logthis( domoticz, debug_level, 'forcastDays is set to  ' .. forcastDays )
                _h.logthis( domoticz, debug_level, 'https://api.open-meteo.com/v1/forecast?latitude=' .. lat .. '&longitude=' .. long .. '&current=temperature_2m,apparent_temperature,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=wind_speed_10m&wind_speed_unit=ms&timezone=auto&forecast_days=' .. forcastDays )
                -- Retrieve the data
                domoticz.openURL({
                    url = 'https://api.open-meteo.com/v1/forecast?latitude=' .. lat .. '&longitude=' .. long .. '&current=temperature_2m,apparent_temperature,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=wind_speed_10m&wind_speed_unit=ms&timezone=auto&forecast_days=' .. forcastDays,
                    method = 'GET',
				    callback = 'meteowind'
                })
            end -- meteoair

            -- For WIND -----------------------------------------
            if triggeredItem.trigger == 'meteowind' then
                -- Check the response and process the data.
                if ( triggeredItem.ok and triggeredItem.isJSON ) then
                    _h.logthis( domoticz, debug_level, 'Item and JSON - OK' )
                    -- We have some result. Store in table.
                    local result_table = triggeredItem.json
                    if type( result_table ) == "table" then
                        _h.logthis( domoticz, debug_level, 'result_table: type = ' .. type( result_table ) )
                        -- Get the current values
                        --"temperature_2m": 10.2,
                        --"apparent_temperature": 6.2,
                        --"wind_speed_10m": 4.71,
                        --"wind_direction_10m": 197,
                        --"wind_gusts_10m": 16.5
                        local temperature = result_table.current.temperature_2m
                        local chill = result_table.current.apparent_temperature
                        local windSpeedMs = _u.round( result_table.current.wind_speed_10m, 1 )
                        local windDirection = result_table.current.wind_direction_10m
                        local windDirectionString = getWindDirectionString( windDirection )
                        local windGust = result_table.current.wind_gusts_10m
                        _h.logthis( domoticz, debug_level, 'Current values - temperature = ' ..  temperature .. '; chill = ' ..  chill .. '; windSpeedMs = ' ..  windSpeedMs ..'; windDirection = ' ..  windDirection .. '; windDirectionString = ' ..  windDirectionString ..'; windGust = ' ..  windGust )

                        intloopCounter = 1
                        local tc = #result_table.hourly.time
                            _h.logthis( domoticz, debug_level, 'Forcasthours = ' .. _d.forcastHours )
                            for i = 1, tc do
                                if result_table.hourly.time[i] > resultTime and intloopCounter <= _d.forcastHours then
                                    _h.logthis( domoticz, debug_level, 'Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - ' .. result_table.hourly.wind_speed_10m[i])
                                    -- Value for the current hour.
                                    meteowindSpeed = result_table.hourly.wind_speed_10m[i]
                                    totalwindspeedValues = totalwindspeedValues + meteowindSpeed                          
                                    intloopCounter = intloopCounter + 1
                                end
                            end
                        -- Calculate and store the average value for future hours in global_data.
                        --NB. This will be used in script d-StookwijzerLokaal.
                        -- Correct intloopCounter for last round
                        intloopCounter = intloopCounter -1                    
                        _d.avgForcastwindspeed = _u.round( totalwindspeedValues / intloopCounter, 1 )
                        _h.logthis( domoticz, debug_level, 'Found average windspeed = ' .. _d.avgForcastwindspeed .. ' in ' .. intloopCounter .. ' records.')
                        
                        -- Now update our meteowinddevice, which triggers d-StookwijzerLokaal script to produce the advice.
                        _h.logthis( domoticz, debug_level, 'Update ' .. domoticz.devices( meteowind_idx ).name  .. '.' )
                        domoticz.devices( meteowind_idx ).updateWind( windDirection, windDirectionString, windSpeedMs, windGust, temperature, chill )                    
                        
                        --Finally update the setpoint if dynamic mode is enabled.
                        if useDynamicSetpoint == 1 then
                            -- Update the setpoint dynamically.
                            _h.logthis( domoticz, debug_level, 'useDynamicSetpoint is ON. Update ' ..  domoticz.devices( setpoint_idx ).name .. ' to ' .. forcastHoursLimit .. '.' )
                            domoticz.devices( setpoint_idx ).updateSetPoint( forcastHoursLimit )
                        end
                    else
                        _h.logthis( domoticz, debug_level, 'No result_table found' )
                    end
    			else
    				_h.logthis( domoticz, debug_level, 'Item or JSON - NOT OK' )
    			end
            end
        end
	end
}
-- That's All --------------------------------------------------
For d-StookwijzerLokaal:
Colorscheme for alerts is changed. See the definition on the site Informatiepunt Leefomgeving.
Code blue no longer exists. They previously avoided the color green, but now apparently want to give even less approval to burning wood in the color code and changed it to yellow.

I comply with these changes in the following manner:
- I think it is more readable to adjust the background color instead of the text color and I have changed that.
- Code blue becomes code yellow. I now show this in the background color of the text, but I keep the alert icon green. You can light the fireplace.
- If pepsOverridemode = 1, then I make the background color darker yellow (between yellow and orange). Then alert icon is still green.
- Code orange and code red remain the same.

Yellow icon was used in case of connection problems. To avoid confusion I decided to use the gray icon for this now.
Colors are set in variables to be able to set it in one place.
I also played with the lineheight in a variable (set to 1.1 now).

Code: Select all

-- 05-01-2023: Script created by Jan peppink, https://ict.peppink.nl
-- Based on information at: https://iplo.nl/thema/lucht/houtstook-stookwijzer-stookalert/#h80dfe1e2-a855-424b-81b4-cdd2bfe71123
-- Gives a treee line result with:
--  Alertcolor, Calculated code (in accordance with the definition on https://iplo.nl), current LKI.
--  Winddescription (Winddirection) windspeed in m/s, bft and km/h.
--  LKI expectation (average for number of coming hours that has been received.
-- NB. Expectation hours counts down when online information is not updatesd.
--     (remains future hours stored online, until we pass the end of it)
-- Depends on LKI device (= local Air Quality Index based on the coordinates).
-- Depends on wind device. (Local windspeed based on given coordinates).
-- Depends on stored average LKI for coming hours stored in global_data.
-- Getting alertLevel by number: 0=gray, 1=green, 2=yellow, 3=orange, 4=red
-- Setting alertLevel by constant:  domoticz.ALERTLEVEL_GREY, ALERTLEVEL_GREEN, ALERTLEVEL_YELLOW, ALERTLEVEL_ORANGE, ALERTLEVEL_RED
-- 25-03-2024 added two variables to override toggle if and when you want to override Orange warning definition. 
--  This is reasonable when LKI is 5, 6 or 7 and windspeed is acceptable.
--  Override is toggled by setting local pepsOverridemode = 1 (0 = off)
--  Acceptable windspeed can be set by local acceptableWindspeed = number
--  I call it 'Code blauw-PLUS' - The alertcolor remains green.
-- 26-03-2024 Improved advice based on windspeed forcast.
--  Depends on stored average Windspeed for coming hours stored in global_data.
--  Calculates forcast advice based on avg LKI and avg wind speed in coming hours.
-- 08-04-2024 Changed color definition. See: https://iplo.nl/thema/lucht/houtstook-de-stookwijzer-en-het-stookalert/
--  I comply with these changes in the following manner:
-- Code blue becomes code yellow. I now show this in the background color of the text, but I keep the alert icon green. You can light the fireplace.
-- If pepsOverridemode = 1, then I make the background color darker yellow (between yellow and orange). Then alert icon is still green.
-- Code orange and code red remain the same.
-- Yellow icon was used in case of connection problems. To avoid confusion I decided to use the gray icon for this now.
-- Colors and lineheight are set in variables.

--- Your settings ----------------------------------------------------------------
-- First set the used device index numbers and variables you might want to change.
local meteowind_idx = n   --idx of your custom meteowind sensor, which triggers this script.
local meteolki_idx = n         -- idx of your 'Luchtkwaliteitsindex' device
local alert_idx = n        -- idx of the Local Stookwijzer sensor to store the advice.

-- Set overrule for situation where LKI between 5 and 6, but windspeed > acceptable windspeed
local pepsOverridemode = 1		-- 1 to enable the override. 0 to turn if off.
local acceptableWindspeed = 5.5	-- Adjust the limit for the wind speed to be used in override mode.

-- loging level 0 = NO logging, 1 = INFO, 2 = DEBUG, 3 = ERROR or 4 = FORCE
local debug_level = 0
        
----------------------------------------------------------------------------------
return {
	on = {
		devices = {
			meteowind_idx,  -- Update of meteowind, after update of LKI, triggers this script to run.
		},
	},
	logging = {
		level = domoticz.LOG_INFO,
		marker = "LokaleStookwijzer-"
	},
	execute = function( domoticz )
		-- Set Local environment=================
        --local _u = domoticz.utilities     -- Holds subset of handy utilities.
		local _h = domoticz.helpers         -- Holds the global functions
		local _d = domoticz.globalData      -- Holds the global data

		-- Get current LKI value
		local lkiValue = domoticz.devices( meteolki_idx ).sensorValue

        -- Use two color variables
        local htmlAdviceColor = '#000000;'  -- Default = Black
        local htmlForcastColor = '#000000;' -- Default = Black\
        local cMagenta = '#ff00ff;'
        local cDarkGray = '#808080;'
        local cGreen = '#008000;'
        local cBlue = '#0000FF;'
        local cYellow = '#ffff00;'
        local cDarkYellow = '#ffdb00;'
        local cOrange = '#ffa500;'
        local cRed = '#ff0000;'	
        --Adjust lineheight
        local lineHeight = 1.1
        -- Local Functions go here =============


		-- Now start to do something ============
		local windSpeedMs = domoticz.devices( meteowind_idx ).speedMs  -- Gets the current value needed for calculation
		local windDirection = domoticz.devices( meteowind_idx ).directionString	-- just for information.

		-- Translate abbreviation into Dutch abbreviation. E(ast) becomes O(ost) S(outh) becomes Z(uid).
		windDirection = string.gsub( windDirection, "E", "O", 2 )
		windDirection = string.gsub( windDirection, "S", "Z", 2 )

		-- Cannot remember from where I got these naming definitions.
		if windSpeedMs >= 0 and windSpeedMs <= 0.2 then windText = '\nWindstil (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596;0 bft &#8596; < 1 km/u' end
		if windSpeedMs >= 0.3 and windSpeedMs <= 1.5 then windText = '\nZeer zwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 1 bft&#8596; 1-5 km/u' end
		if windSpeedMs >= 1.6 and windSpeedMs <= 3.3 then windText = '\nZwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 2 bft &#8596; 6-11 km/u' end
		if windSpeedMs >= 3.4 and windSpeedMs <= 5.4 then windText = '\nZeer matige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 3 bft &#8596; 12-19 km/u' end
		if windSpeedMs >= 5.5 and windSpeedMs <= 7.9 then windText = '\nMatige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 4 bft &#8596; 20-28 km/u' end
		if windSpeedMs >= 8.0 and windSpeedMs <= 10.7 then windText = '\nVrij krachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 5 bft &#8596; 29-38 km/u' end
		if windSpeedMs >= 10.8 and windSpeedMs <= 13.8 then windText = '\nKrachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 6 bft &#8596; 39-49 km/u' end
		if windSpeedMs >= 13.9 and windSpeedMs <= 17.1 then windText = '\nHarde wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 7 bft &#8596; 50-61 km/u' end
		if windSpeedMs >= 17.2 and windSpeedMs <= 20.7 then windText = '\nStormachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 8 bft &#8596; 62-74 km/u' end
		if windSpeedMs >= 20.8 and windSpeedMs <= 24.4 then windText = '\nStorm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 9 bft &#8596; 75-88 km/u' end
		if windSpeedMs >= 24.5 and windSpeedMs <= 28.4 then windText = '\nZware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 10 bft &#8596; 89-102 km/u' end
		if windSpeedMs >= 28.5 and windSpeedMs <= 32.6 then windText = '\nZeer zware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 11 bft &#8596; 103-117 km/u' end
		if windSpeedMs > 32.6 then windText = '\nOrkaan (' .. windDirection .. ') ' .. windSpeedMs .. ' m/s &#8596; 12 bft &#8596; >117 km/u' end
        --Add lineheight
        windText = '<span style="line-height:' .. lineHeight .. ';">' .. windText .. '</span>'

        -- Prepare the forecast prefix sign
		-- Set the sign for expected increase or decrease. arrowup = &#8593; arrowdown = &#8595; equal = &#8596;
        local lkiSign = '<strong><span style="color: ' .. cMagenta .. '">&#8596;</span></strong>'
		if _d.avgForcastLKI > lkiValue then
		      lkiSign = '<strong><span style="color: ' .. cRed .. '">&#9650;</span></strong>'
		elseif _d.avgForcastLKI < lkiValue then
		      lkiSign = '<strong><span style="color: ' .. cGreen .. '">&#9660;</span></strong>'
		end
        local windSign = '<strong><span style="color: ' .. cMagenta .. '">&#8596;</span></strong>'
		if _d.avgForcastwindspeed > windSpeedMs then
		      windSign =  '<strong><span style="color: ' .. cGreen .. '">&#9650;</span></strong>'
		elseif  _d.avgForcastwindspeed < windSpeedMs then
		      windSign = '<strong><span style="color: ' .. cRed .. '">&#9660;</span></strong>'
		end
       
        -- Calculate and set the current code.
		if _d.forcastHours == 0 and _d.avgForcastLKI == 0 then
			--Code Gray; No LKI data for current hour received.
			alertText = 'Niet beschikbaar!! Laatste LKI was '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_GRAY
		elseif lkiValue <= 4 and windSpeedMs > 2 then
			--Code = Yellow; You can light the fireplace, but take the neighbors into account.
			--LKI <= 4 and windsspeed > 2m/sec.
			htmlAdviceColor = cYellow
			alertText = '<span style="background-color: ' .. htmlAdviceColor .. '">Code geel.</span> LKI: '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_GREEN
		elseif lkiValue >= 5 and lkiValue <=7 and windSpeedMs > 2 then
			--Code Orange; It is better not to burn wood now.
			--LKI >= 5 and <= 7 and windspeed > 2m/sec.
            htmlAdviceColor = cOrange
			alertText = '<span style="background-color: ' .. htmlAdviceColor .. '">Code oranje.</span> LKI: '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_ORANGE
		elseif lkiValue >= 8 or windSpeedMs <= 2 then
			--Code Red; Do not burn wood.
			--LKI >= 8 and/or the windspeed is low (<= 2m/sec).
            htmlAdviceColor = cRed
			alertText = '<span style="background-color: ' .. htmlAdviceColor .. '">Code rood.</span> LKI: '  .. lkiValue .. windText
			alertLevel = domoticz.ALERTLEVEL_RED
		end

        -- Set the forcast colors
        if _d.avgForcastLKI <= 4 and _d.avgForcastwindspeed > 2 then
            htmlForcastColor = cYellow
        elseif _d.avgForcastLKI >= 5 and _d.avgForcastLKI <= 7 and _d.avgForcastwindspeed > 2 then
            htmlForcastColor = cOrange
        elseif _d.avgForcastLKI >= 8 or _d.avgForcastwindspeed <= 2 then
            htmlForcastColor = cRed
        end

		-- You may want to override the code orange, when there is a certain wind speed.
		if pepsOverridemode == 1 then
		    -- For current advice.
		    if lkiValue >= 5 and lkiValue <=7 and windSpeedMs >= acceptableWindspeed then
			    -- Added personal level Blauw-PLUS. LKI is higher, but there is wind.
		        htmlAdviceColor = cDarkYellow
			    --Code = Blauw; You can light the fireplace, but take the neighbors into account.
			    alertText = '<span style="background-color: ' ..  htmlAdviceColor .. '">Code geel-PLUS</span> LKI: '  .. lkiValue .. windText
			    alertLevel = domoticz.ALERTLEVEL_GREEN
            end
            -- For the forecast advice color.
            if  _d.avgForcastLKI >= 5 and _d.avgForcastLKI <= 7 and _d.avgForcastwindspeed >= acceptableWindspeed then
			    htmlForcastColor = cDarkYellow
			end
		end

		-- Generate the new forecast.
        alertText = alertText .. '\n<span style="line-height:' .. lineHeight .. '; background-color: ' .. htmlForcastColor .. '"> x&#772; verwachting</span> <span style="color: ' .. cBlue .. '">' .. _d.forcastHours .. '</span> uur: LKI' .. lkiSign .. _d.avgForcastLKI .. ' en wind' .. windSign .. _d.avgForcastwindspeed .. ' m/s'			

		-- Now finally update with new alertLevel and alertText we have found.
        _h.logthis( domoticz, debug_level, 'Update ' .. domoticz.devices( alert_idx ).name  .. '.' )
		domoticz.devices( alert_idx ).updateAlertSensor( alertLevel, alertText )
	end
}
-- That's All --------------------------------------------------
Example of the result:
LocaleStookwijzer-JanPep-3.png
LocaleStookwijzer-JanPep-3.png (19.4 KiB) Viewed 3792 times
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

Example where the RIVM alert for the province does not give a warning message, while local conditions with a wind speed of 1.3 m/s give a red alert.
LocaleStookwijzer-JanPep-4.png
LocaleStookwijzer-JanPep-4.png (40.16 KiB) Viewed 3770 times
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

And then another with a longer forecast.
LocaleStookwijzer-JanPep-5.png
LocaleStookwijzer-JanPep-5.png (38.7 KiB) Viewed 3757 times
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
User avatar
Thuis
Posts: 270
Joined: Tuesday 11 September 2018 11:36
Target OS: Linux
Domoticz version: Beta
Location: The Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by Thuis »

Hey, this handy handig, now i can show my neighbour when he has to stop smoking us out of our house with his everything burner. It works great. I have code geel now and lki of 4, setpoint is now 9 hours.

Im thinking of adding a siren with a light to it, to hang outside of my house, so the neighbours can see when they are smoking their allesbrander. Does it also account for the mist and clouds ? Oh well i think it is very handy thanks.
I Love Domoticz ! And the community around it :-)
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

It takes local Air quality index and Windspeed.
My intention was to take my neighbors into account. Not (the other way around) to beat them with the outcome :-). There is no prohibition, by the way.
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
User avatar
Thuis
Posts: 270
Joined: Tuesday 11 September 2018 11:36
Target OS: Linux
Domoticz version: Beta
Location: The Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by Thuis »

Well i do not know why they would burn wood and other stuff in the year 2024, and not just use their CV or a heatpump or AC. But thats just me. My whole house is smoked out because they need to burn their fireplace. I have to close all the windows and ventilation. And now, in the summer time, they have a patio heater on wood too. So when it is like 25+ degrees inside, celcius that is, then we need to close everything too because of the smoke and the smell. So now i just connect a siren and a blink light, and see what happens. And yes i tried talking, but it is their right to burn wood, so they say. My kids get copd of it but they dont mind, they have the right to burn the wood. So thanks for the script :-)
I Love Domoticz ! And the community around it :-)
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

I changed the two major scripts.
d-StookwijzerLokaal for cosmetic reasons and dt-OpenMeteo for the behaviour of the dynamic setpoint.
I'd rather work on the functionality and the code that goes with it, than mess around with the looks. That always takes the most of my time, while I am rarely satisfied with it. :-)

1. d-StookwijzerLokaal
I was not happy with the appearance of colors and background. It did not look nice. Also I did not like the code text. So I have changed the colors and alert text. Also more in line with New Dutch DzVents RIVM Stookalert.
- Also more flexible. You can now set both background and foreground color in variables as you wish.
- I translate code yellow to green (= Geen stookalert). With code yellow they avoid green as a Clearly allowed. :-)
- If pepsOverridemode = 1 (= Lichte waarschuwing stookalert), then darker yellow (not yet orange). Normally LKI of 5 would give code Orange. I created this extra level. Now with Wind > 5.5 m/s it becomes DarkYellow on gray background for readability. Then the alert icon is still green.
- Code orange (= Waaarschuwing stookalert.)
- Code red (= Stookalert!).
The forcast still gives the calculated average for the coming hours while the little arrows up and down with LKI and wind show if the value will increase or decrease (green = better, red = worse). This remained the same.

Here is the script d-StookwijzerLokaal:

Code: Select all

-- 05-01-2023: Script created by Jan Peppink, https://ict.peppink.nl
-- Based on information at: https://iplo.nl/thema/lucht/houtstook-stookwijzer-stookalert/#h80dfe1e2-a855-424b-81b4-cdd2bfe71123
-- Gives a treee line result with:
--  Alertcolor, Calculated code (in accordance with the definition on https://iplo.nl), current LKI.
--  Winddescription (Winddirection) windspeed in m/s, bft and km/h.
--  LKI expectation (average for number of coming hours that has been received.
-- NB. Expectation hours counts down when online information is not updatesd.
--     (remains future hours stored online, until we pass the end of it)
-- Depends on LKI device (= local Air Quality Index based on the coordinates).
-- Depends on wind device. (Local windspeed based on given coordinates).
-- Depends on stored average LKI for coming hours stored in global_data.
-- Getting alertLevel by number: 0=gray, 1=green, 2=yellow, 3=orange, 4=red
-- Setting alertLevel by constant:  domoticz.ALERTLEVEL_GREY, ALERTLEVEL_GREEN, ALERTLEVEL_YELLOW, ALERTLEVEL_ORANGE, ALERTLEVEL_RED
-- 25-03-2024 added two variables to override toggle if and when you want to override Orange warning definition. 
--  This is reasonable when LKI is 5, 6 or 7 and windspeed is acceptable.
--  Override is toggled by setting local pepsOverridemode = 1 (0 = off)
--  Acceptable windspeed can be set by local acceptableWindspeed = number
--  I call it 'Code blauw-PLUS' - The alertcolor remains green.
-- 26-03-2024 Improved advice based on windspeed forcast.
--  Depends on stored average Windspeed for coming hours stored in global_data.
--  Calculates forcast advice based on avg LKI and avg wind speed in coming hours.
-- 08-04-2024 Changed color definition. See: https://iplo.nl/thema/lucht/houtstook-de-stookwijzer-en-het-stookalert/
--  Colors and lineheight are set in variables.
-- 29-05-2024 Directly make use of dz.log instead of the 'logthis' function in global_data.
-- 29-05-2024 Replaced \n by <br> for html string, to let following lines also appear in the log.
-- 13-02-2025 Changed the colors and alert tex. It did not look nice. Also did not like the code text.
--  You can set both background and foreground color in variables as you wish.
--  I translate code yellow to green (Geen stookalert). With yellow they avoid green as a Clearly allowed. :-)
--  If pepsOverridemode = 1 (Lichte waarschuwing stookalert), then darker yellow (not yet orange). Then alert icon is still green.
--  Code orange (Waaarschuwing stookalert.)
--  Code red (Stookalert!).


--- Your settings ----------------------------------------------------------------
-- First set the used device index numbers and variables you might want to change.
local meteowind_idx = 99999   --idx of your custom meteowind sensor, which triggers this script.
local meteolki_idx = 99999         -- idx of your 'Luchtkwaliteitsindex' device
local alert_idx = 99999        -- idx of the Local Stookwijzer sensor to store the advice.

-- Set overrule for situation where LKI between 5 and 6, but windspeed > acceptable windspeed
local pepsOverridemode = 1		-- 1 to enable the override. 0 to turn if off.
local acceptableWindspeed = 5.5	-- Adjust the limit for the wind speed to be used in override mode.

----------------------------------------------------------------------------------
return {
	on = {
		devices = {
			meteowind_idx,  -- Update of meteowind, after update of LKI, triggers this script to run.
		},
	},
	logging = {
		-- Level can be domoticz.LOG_INFO, domoticz.LOG_STATUS, domoticz.LOG_ERROR or domoticz.LOG_DEBUG
		level = domoticz.LOG_STATUS,
		--level = domoticz.LOG_DEBUG,
		marker = "LokaleStookwijzer-"
	},
	execute = function( dz, triggeredItem )
		-- Set Local environment=================
        --local _u = dz.utilities     -- Holds subset of handy utilities.
		--local _h = dz.helpers         -- Holds the global functions
		local _d = dz.globalData      -- Holds the global data

		-- Get current LKI value
		local lkiValue = dz.devices( meteolki_idx ).sensorValue

        -- Use some color variables
        local htmlAdviceColor = '#000000;'  -- Default = Black
        local htmlForcastColor = '#000000;' -- Default = Black\
        local cWhite = '#FFFFFF;'
        local cMagenta = '#ff00ff;'
        local cDarkGray = '#808080;'
        local cLightGray = '#d8d8d8;'
        local cGreen = '#008000;'
        local cBlue = '#0000FF;'
        local cYellow = '#ffff00;'
        local cDarkYellow = '#ffdb00;'
        local cOrange = '#ffa500;'
        local cRed = '#ff0000;'	
        local cTheme = '#F1F5FA;' -- Default theme background color
        local htmlDarkBackgroundColor = cDarkGray
        local htmlLightBackgroundColor = cTheme
        
        --Adjust lineheight
        local lineHeight = 1.1
        -- Local Functions go here =============


		-- Now start to do something ============
		local windSpeedMs = dz.devices( meteowind_idx ).speedMs  -- Gets the current value needed for calculation
		local windDirection = dz.devices( meteowind_idx ).directionString	-- just for information.
    		-- Translate abbreviation into Dutch abbreviation. E(ast) becomes O(ost) S(outh) becomes Z(uid).
    		windDirection = string.gsub( windDirection, "E", "O", 2 )
    		windDirection = string.gsub( windDirection, "S", "Z", 2 )

		-- Cannot remember from where I got these naming definitions.
		if windSpeedMs >= 0 and windSpeedMs <= 0.2 then windText = '<br>Windstil (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596;0 bft &#8596; < 1 km/u'
		elseif windSpeedMs >= 0.3 and windSpeedMs <= 1.5 then windText = '<br>Zeer zwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 1 bft&#8596; 1-5 km/u'
		elseif windSpeedMs >= 1.6 and windSpeedMs <= 3.3 then windText = '<br>Zwakke wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 2 bft &#8596; 6-11 km/u'
		elseif windSpeedMs >= 3.4 and windSpeedMs <= 5.4 then windText = '<br>Zeer matige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 3 bft &#8596; 12-19 km/u'
		elseif windSpeedMs >= 5.5 and windSpeedMs <= 7.9 then windText = '<br>Matige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 4 bft &#8596; 20-28 km/u'
		elseif windSpeedMs >= 8.0 and windSpeedMs <= 10.7 then windText = '<br>Vrij krachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 5 bft &#8596; 29-38 km/u'
		elseif windSpeedMs >= 10.8 and windSpeedMs <= 13.8 then windText = '<br>Krachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 6 bft &#8596; 39-49 km/u'
		elseif windSpeedMs >= 13.9 and windSpeedMs <= 17.1 then windText = '<br>Harde wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 7 bft &#8596; 50-61 km/u'
		elseif windSpeedMs >= 17.2 and windSpeedMs <= 20.7 then windText = '<br>Stormachtige wind (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 8 bft &#8596; 62-74 km/u'
		elseif windSpeedMs >= 20.8 and windSpeedMs <= 24.4 then windText = '<br>Storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 9 bft &#8596; 75-88 km/u'
		elseif windSpeedMs >= 24.5 and windSpeedMs <= 28.4 then windText = '<br>Zware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 10 bft &#8596; 89-102 km/u'
		elseif windSpeedMs >= 28.5 and windSpeedMs <= 32.6 then windText = '<br>Zeer zware storm (' .. windDirection .. ') ' .. windSpeedMs .. 'm/s &#8596; 11 bft &#8596; 103-117 km/u'
		elseif windSpeedMs > 32.6 then windText = '<br>Orkaan (' .. windDirection .. ') ' .. windSpeedMs .. ' m/s &#8596; 12 bft &#8596; >117 km/u' end
        --Add lineheight
        windText = '<span style="line-height:' .. lineHeight .. ';">' .. windText .. '</span>'

        -- Prepare the forecast prefix sign
		-- Set the sign for expected increase or decrease. arrowup = &#8593; arrowdown = &#8595; equal = &#8596;
        local lkiSign = '<strong><span style="color: ' .. cMagenta .. '">&#8596;</span></strong>'
		if _d.avgForcastLKI > lkiValue then
		      lkiSign = '<strong><span style="color: ' .. cRed .. '">&#9650;</span></strong>'
		elseif _d.avgForcastLKI < lkiValue then
		      lkiSign = '<strong><span style="color: ' .. cGreen .. '">&#9660;</span></strong>'
		end
        local windSign = '<strong><span style="color: ' .. cMagenta .. '">&#8596;</span></strong>'
		if _d.avgForcastwindspeed > windSpeedMs then
		      windSign =  '<strong><span style="color: ' .. cGreen .. '">&#9650;</span></strong>'
		elseif  _d.avgForcastwindspeed < windSpeedMs then
		      windSign = '<strong><span style="color: ' .. cRed .. '">&#9660;</span></strong>'
		end
       
        -- Calculate and set the current code.
		if _d.forcastHours == 0 and _d.avgForcastLKI == 0 then
			--Code Gray; No LKI data for current hour received.
			alertText = 'Niet beschikbaar!! Laatste LKI was '  .. lkiValue .. windText
			alertLevel = dz.ALERTLEVEL_GRAY
		elseif lkiValue <= 4 and windSpeedMs > 2 then
			--Code = Yellow; You can light the fireplace, but take the neighbors into account. I set the Yellow to Green.
			--LKI <= 4 and windsspeed > 2m/sec.
			htmlAdviceColor = cGreen
			htmlBackgroundColor = htmlLightBackgroundColor
			alertText = '<span style="background-color: ' .. htmlBackgroundColor  .. '; color: ' ..  htmlAdviceColor .. ';">Geen stookalert.</span> LKI: '  .. lkiValue .. windText
			alertLevel = dz.ALERTLEVEL_GREEN
		elseif lkiValue >= 5 and lkiValue <=7 and windSpeedMs > 2 then
			--Code Orange; It is better not to burn wood now.
			--LKI >= 5 and <= 7 and windspeed > 2m/sec.
            htmlAdviceColor = cOrange
            htmlBackgroundColor = htmlLightBackgroundColor
			alertText = '<span style="background-color: ' ..  htmlBackgroundColor .. '; color: ' ..  htmlAdviceColor .. ';">Waaarschuwing stookalert.</span> LKI: '  .. lkiValue .. windText
			alertLevel = dz.ALERTLEVEL_ORANGE
		elseif lkiValue >= 8 or windSpeedMs <= 2 then
			--Code Red; Do not burn wood.
			--LKI >= 8 and/or the windspeed is low (<= 2m/sec).
            htmlAdviceColor = cRed
            htmlBackgroundColor = htmlLightBackgroundColor
			alertText = '<span style="background-color: ' ..  htmlBackgroundColor .. '; color: ' .. htmlAdviceColor  .. ';">Stookalert!</span> LKI: '  .. lkiValue .. windText
			alertLevel = dz.ALERTLEVEL_RED
		end

        -- Set the forcast colors
        if _d.avgForcastLKI <= 4 and _d.avgForcastwindspeed > 2 then
            htmlForcastColor = cGreen
        elseif _d.avgForcastLKI >= 5 and _d.avgForcastLKI <= 7 and _d.avgForcastwindspeed > 2 then
            htmlForcastColor = cOrange
        elseif _d.avgForcastLKI >= 8 or _d.avgForcastwindspeed <= 2 then
            htmlForcastColor = cRed
        end

		-- You may want to override the code orange, when there is a certain wind speed.
		if pepsOverridemode == 1 then
		    -- For current advice.
		    if lkiValue >= 5 and lkiValue <=7 and windSpeedMs >= acceptableWindspeed then
			    -- Added personal level Blauw-PLUS. LKI is higher, but there is wind.
		        htmlAdviceColor = cDarkYellow
		        htmlBackgroundColor = htmlDarkBackgroundColor
			    --Code = Blauw; You can light the fireplace, but take the neighbors into account.
			    --alertText = '<span style="background-color: ' ..  htmlAdviceColor .. '">Code geel-PLUS</span> LKI: '  .. lkiValue .. windText
			    alertText = '<span style="background-color: ' ..  htmlBackgroundColor .. '; color: ' ..  htmlAdviceColor .. ';">Lichte waarschuwing stookalert.</span> LKI: '  .. lkiValue .. windText
			    alertLevel = dz.ALERTLEVEL_GREEN
            end
            -- For the forecast advice color.
            if  _d.avgForcastLKI >= 5 and _d.avgForcastLKI <= 7 and _d.avgForcastwindspeed >= acceptableWindspeed then
			    htmlForcastColor = cDarkYellow
			    htmlBackgroundColor = htmlDarkBackgroundColor
			end
		end

		-- Generate the new forecast.
        alertText = alertText .. '<br><span style="line-height:' .. lineHeight .. '; background-color: ' ..  htmlBackgroundColor .. '; color: ' .. htmlForcastColor .. '"> x&#772; verwachting</span> <span style="color: ' .. cBlue .. '">' .. _d.forcastHours .. '</span> uur: LKI' .. lkiSign .. _d.avgForcastLKI .. ' en wind' .. windSign .. _d.avgForcastwindspeed .. ' m/s'

		-- Now finally update with new alertLevel and alertText we have found.
        dz.log( 'Update ' .. dz.devices( alert_idx ).name  .. '.', dz.LOG_DEBUG )
		dz.devices( alert_idx ).updateAlertSensor( alertLevel, alertText )
	end
}
-- That's All --------------------------------------------------
2. dt-OpenMeteo
The shorter the time, the more acurate the forecast.
Normally I prefer the forecast until the end of the current day. So I always use useDynamicSetpoint = 1.
In the previous version the dynamic setpoint disabled the function of the manual setting of the setpoint.
Sometimes I wanted to look further ahead. That didn't work easily. I had to change the variable setting.
Now I changed the behaviour of the setpoint device with the use of Dynamic setpoint.
When useDynamicSetpoint = 0, It uses this setpoint while triggered by time or by change of the setpoint.
When useDynamicSetpoint = 1, it calculates remaining hours until end of the day every time it runs (by time trigger)
But now can also directly be triggered when changing the setpoint using that value.
(The next time triggered by time, it continues with the dynamic setting).

Here is the script dt-OpenMeteo:

Code: Select all

-- 26-03-2024: Script created by Jan Peppink, https://ict.peppink.nl
-- Get data from https://open-meteo.com/
-- Free and without api key for non-commercial use and less than 10.000 daily API calls per day. (See other terms of use on their website).
-- Created URLs are visible in log when debug_level is set.
-- Script is triggered every hour on timer setting and also on change of the setpoint device (wanted #hours).
-- Setpoint can be set between 1 and 72 hour.
-- When you set useDynamicSetpoint = 1, the forcast is calculated until 00:00 hour and the setpoint dynamically counts down.
-- In ths case manually changing the Setpoint has no effect and will be overwritten next round.
-- Gets data in two rounds with multiple callbacks, because there is a separate api for weater(wind) and for airquality.
-- 1. Gets the current and forcast ozone, pm10, pm2_5, nitrogen_dioxide data for current location
--    Calculates LKI for current time and average LKI for the forcastHours.
--    Saves LKI value in LKI device and average in global_data
-- 2. Gets the current wind data and forecast wind speed for current location.
--    Saves wind values in wind device and average in global_data.
--    Updates setpoint device with current value, when useDynamicSetpoint = 1.
-- Works with _d.forcastHours, _d.avgForcastLKI and _d.avgForcastwindspeed from global_data
-- Added option to useDynamicSetpoint until midnight. This overrules manually set Setpoint and also ignores device trigger.
-- 07-04-2024: Changed forcastHours calculation. Fix for 23 hr. when useDynamicSetpoint = 1 to set forcastHoursLimit = 1.
-- 10-04-2024: Fixed bug that caused a loop when triggered on device and useDynamicSetpoint == 0.
-- 25-05-2024 Removed function 'getWindDirectionString'. Instead call _h.getDirectionfromDegree in global_data
-- 29-05-2024 Directly make use of dz.log instead of the 'logthis' function in global_data.
-- 13-02-2025 Changed use of the setpoint device with the use of Dynamic setpoint.
--  A. To use only the setpoint device, set useDynamicSetpoint to 0
--  B. When useDynamicSetpoint = 1, it calculates remaining hours until end of the day every time it runs, BUT
--  C. Now can also directly be triggered when changing the setpoint(, while next hour continues with the dynamic setting).

--- Your settings ----------------------------------------------------------------
-- First set the used device index numbers and variables you might want to change.
local meteolki_idx = 99999        -- idx of your custom sensor 'Luchtkwaliteitsindex'.
local setpoint_idx = 99999       -- idx of your custom setpoint device.
local meteowind_idx = 99999      -- idx of your custom meteowind sensor
local useDynamicSetpoint = 1    -- 1 = Limit forcast until 00:00 hour and setpoint when changing or 0 = Use Setpoint only.

----------------------------------------------------------------------------------
return {
	on = {
		timer = { 
			--'every 2 minutes',    -- Only used for testing.
            'at *:02',              -- (LKI info online is updated at the hour)
        },
		devices = {
			setpoint_idx,	-- idx of your setpoint for wanted forcastHours
		},
		httpResponses = {
            'meteoair',       -- matches callback string below
            'meteowind'       -- matches callback string below
        },
	},
	logging = {
	    -- Level can be domoticz.LOG_INFO, domoicz.LOG_MODULE_EXEC_INFO, domoticz.LOG_DEBUG, domoticz.LOG_ERROR or domoticz.LOG_FORCE
		-- Level can be domoticz.LOG_INFO, domoticz.LOG_STATUS, domoticz.LOG_ERROR or domoticz.LOG_DEBUG
		level = domoticz.LOG_STATUS,
		--level = domoticz.LOG_DEBUG,
		marker = 'OpenMeteo-',
	},
	execute = function( dz, triggeredItem )
	    
        -- Set Local environment=================
        local _u = dz.utils       -- Holds subset of handy utilities.
        local _h = dz.helpers     -- Holds the global functions.
        local _d = dz.globalData  -- Holds the global data.

        -- Get location coordinates
        local lat = dz.settings.location.latitude
        local long = dz.settings.location.longitude

        -- currenTime = RawDateTime = e.g. 2022-12-06 19:50:00
        local currentTime = tostring( dz.time.rawDateTime )
        -- resultTime comes back in json result e.g. "2024-03-26T00:00"
        local resultTime = dz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'yyyy-mm-ddThh:00', 0 )
        local nextHour = dz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'hh', 3600 )

        -- For air and Wind data
        local pm10 = 0
        local pm2_5 = 0
        local nitrogen_dioxide = 0
        local ozone = 0
        local lki = 0
        local totallkiValues = 0    -- Will hold a count of the total of the values we get.
        local intloopCounter = 0    -- Will hold a count of the the number of the values we get.

        local meteowindSpeed = dz.devices( meteowind_idx ).speedMs  -- Gets last stored value.
        local totalwindspeedValues = 0    -- Will hold a count of the total of the values we get.

        -- Use Setpoint as the default for the forcastHours calculation
        local forcastDays = 2 --2 is default
        local forcastHoursLimit = _u.round( dz.devices( setpoint_idx ).setPoint, 0 )
        dz.log( 'Setpoint = ' .. forcastHoursLimit, dz.LOG_DEBUG )
        
        -- Override default only when set to use dynamically AND setpoint is not recently changed.
        if useDynamicSetpoint == 1 and dz.devices(setpoint_idx).lastUpdate.minutesAgo > 3 then
            -- Calculate remaining hous untill (including) 00:00 o'clock, which is next day.
            dz.log( 'currentTime = ' .. currentTime .. ' and nextHour = ' .. nextHour, dz.LOG_DEBUG )
            if dz.time.dateToDate( currentTime, 'yyyy-mm-dd hh:MM:ss', 'hh', 0 ) == '23' then
                -- Exeption! nexthour '00' does not fit with forcastDays calculation below.
                -- Cannot add '00' hr. Therefore set it nextHour to 24, which is 1 hour from now.
                nextHour = 24
                forcastHoursLimit = 1
                dz.log( 'For dynamic setpoint calculation correct nextHour to ' .. nextHour .. ' and set forcatHoursLimit to '  ..forcastHoursLimit, dz.LOG_DEBUG )
            else
                forcastHoursLimit = _u.round( 25 - nextHour, 0 )
            end
        end
        if nextHour + forcastHoursLimit >= 48 then forcastDays = 3 end
        if nextHour + forcastHoursLimit < 48 then forcastDays = 2 end
        if nextHour + forcastHoursLimit < 24 then forcastDays = 1 end

        -- Local Functions go here =============
        local function calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
            --To calculate the LKI index. See https://www.rivm.nl/bibliotheek/rapporten/2014-0050.pdf
            -- The component that falls into the highest category determines the value of the index.
            lki = 0
            --INDEX             1	    2	    3	    4	    5	    6	    7	    8	    9	    10
            --ozone	            0-15	15-30	30-40	40-60	60-80	80-100	100-140	140-180	180-200	> 200
            --pm10              0-10	10–20	20–30	30-45	45-60	60-75	75-100	100-125	125-150	> 150
            --pm2_5             0-10	10-15	15-20	20-30	30-40	40-50	50-70	70-90	90-100	> 100
            --nitrogen_dioxide	0-10	10-20	20-30	30-45	45-60	60-75	75-100	100-125	125-150	> 150 
            if ozone > 0 and ozone <=15 then if lki < 1 then lki = 1 end end
            if pm10 > 0 and pm10 <= 10 then if lki < 1 then lki = 1 end end
            if pm2_5 > 0 and pm2_5 <= 10 then if lki < 1 then lki = 1 end end
            if nitrogen_dioxide  > 0 and nitrogen_dioxide <= 10 then if lki < 1 then lki = 1 end end
            --dz.log( 'LKI step 1 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 15 and ozone <= 30 then if lki < 2 then lki = 2 end end
            if pm10 > 10 and pm10 <= 20 then if lki < 2 then lki = 2 end end
            if pm2_5 > 10 and pm2_5 <= 15 then if lki < 2 then lki = 2 end end
            if nitrogen_dioxide > 10 and nitrogen_dioxide <= 20 then if lki < 2 then lki = 2 end end
            --dz.log( 'LKI step 2 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 30 and ozone <= 40 then if lki < 3 then lki = 3 end end
            if pm10 > 20 and pm10 <= 30 then if lki < 3 then lki = 3 end end
            if pm2_5 > 15 and pm2_5 <= 20 then if lki < 3 then lki = 3 end end
            if nitrogen_dioxide > 20 and nitrogen_dioxide <= 30 then if lki < 3 then lki = 3 end end
            --dz.log( 'LKI step 3 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 40 and ozone <= 60 then if lki < 4 then lki = 4 end end
            if pm10 > 30 and pm10 <= 45 then if lki < 4 then lki = 4 end end
            if pm2_5 > 20 and pm2_5 <= 30 then if lki < 4 then lki = 4 end end
            if nitrogen_dioxide > 30 and nitrogen_dioxide <= 45 then if lki < 4 then lki = 4 end end
            --dz.log( 'LKI step 4 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 60 and ozone <= 80 then if lki < 5 then lki = 5 end end
            if pm10 > 45 and pm10 <= 60 then if lki < 5 then lki = 5 end end
            if pm2_5 > 30 and pm2_5 <= 40 then if lki < 5 then lki = 5 end end
            if nitrogen_dioxide > 45 and nitrogen_dioxide <= 60 then if lki < 5 then lki = 5 end end
            --dz.log( 'LKI step 5 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 80 and ozone <= 100 then if lki < 6 then lki = 6 end end
            if pm10 > 60 and pm10 <= 75 then if lki < 6 then lki = 6 end end
            if pm2_5 > 40 and pm2_5 <= 50 then if lki < 6 then lki = 6 end end
            if nitrogen_dioxide > 60 and nitrogen_dioxide <= 75 then if lki < 6 then lki = 6 end end
            --dz.log( 'LKI step 6 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 100 and ozone <= 140 then if lki < 7 then lki = 7 end end
            if pm10 > 75 and pm10 <= 100 then if lki < 7 then lki = 7 end end
            if pm2_5 > 50 and pm2_5 <= 70 then if lki < 7 then lki = 7 end end
            if nitrogen_dioxide > 75 and nitrogen_dioxide <= 100 then if lki < 7 then lki = 7 end end
            --dz.log( 'LKI step 7 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 140 and ozone <= 180 then if lki < 8 then lki = 8 end end
            if pm10 > 100 and pm10 <= 125 then if lki < 8 then lki = 8 end end
            if pm2_5 > 70 and pm2_5 <= 90 then if lki < 8 then lki = 8 end end
            if nitrogen_dioxide > 100 and nitrogen_dioxide <= 125 then if lki < 8 then lki = 8 end end
            --dz.log( 'LKI step 8 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 180 and ozone <= 200 then if lki < 9 then lki = 9 end end
            if pm10 > 125 and pm10 <= 150 then if lki < 9 then lki = 9 end end
            if pm2_5 > 90 and pm2_5 <= 100 then if lki < 9 then lki = 9 end end
            if nitrogen_dioxide > 125 and nitrogen_dioxide <= 150 then if lki < 9 then lki = 9 end end
            --dz.log( 'LKI step 9 = ' ..  lki, dz.LOG_DEBUG )
            if ozone > 200 then lki = 10 end
            if pm10 > 150 then lki = 10 end
            if pm2_5 > 100 then lki = 10 end
            if nitrogen_dioxide > 150 then lki = 10 end
            --dz.log( 'LKI step 10 = ' ..  lki, dz.LOG_DEBUG )
            return lki
        end

        -- Now start to do something ===========================
        -- Get the data for air.
		if ( triggeredItem.isTimer or triggeredItem.isDevice ) then
            -- Log what we do.
            dz.log( '#forcastDays to retreive is set to  ' .. forcastDays, dz.LOG_DEBUG )
            -- Retrieve the data
            dz.openURL({
                url = 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=' .. lat .. '&longitude=' .. long .. '&current=pm10,pm2_5,nitrogen_dioxide,ozone&hourly=pm10,pm2_5,nitrogen_dioxide,ozone&timezone=auto&forecast_days=' .. forcastDays,
                method = 'GET',
                callback = 'meteoair'
            })
		end	

        -- Process the obtained data.
        if ( triggeredItem.isHTTPResponse ) then
            -- For AIR ------------------------------------------
            if triggeredItem.trigger == 'meteoair' then
                -- Check the response and process the data.
                if ( triggeredItem.ok and triggeredItem.isJSON ) then
                    dz.log( 'Item and JSON - OK', dz.LOG_DEBUG )
                    -- We have some result. Store in table.
                    local result_table = triggeredItem.json
                    if type( result_table ) == "table" then
                        dz.log( 'result_table: type = ' .. type( result_table ), dz.LOG_DEBUG )
                        -- Now loop simultaneously trhough the hourly tables for the forcast
                        local tc = #result_table.hourly.time
                        for i = 1, tc do
                            -- Only take the future windspeed for the number of hours we have also for the LKI in _d.forcastHours.
                            if intloopCounter < 1 and result_table.hourly.time[i] == resultTime then
                                -- This is for the current hour.
                                pm10 = result_table.hourly.pm10[i]
                                pm2_5 = result_table.hourly.pm2_5[i]
                                nitrogen_dioxide = result_table.hourly.nitrogen_dioxide[i]
                                ozone = result_table.hourly.ozone[i]
                                lki = calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                dz.log( 'CurrentTime Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - LKI = ' ..lki  .. '; pm10 = ' ..  pm10 .. '; pm2_5 = ' ..  pm2_5 .. '; nitrogen_dioxide = ' ..  nitrogen_dioxide ..'; ozone = ' ..  ozone, dz.LOG_DEBUG )
                                -- Update sensor with current LKI value
                                dz.log( 'Update ' .. dz.devices( meteolki_idx ).name .. '.', dz.LOG_DEBUG )
                                dz.devices( meteolki_idx ).updateCustomSensor( lki )
                                intloopCounter = intloopCounter + 1
                            end
                            if result_table.hourly.time[i] > resultTime and intloopCounter <= forcastHoursLimit then
                                -- This is for the forcast
                                pm10 = result_table.hourly.pm10[i]
                                pm2_5 = result_table.hourly.pm2_5[i]
                                nitrogen_dioxide = result_table.hourly.nitrogen_dioxide[i]
                                ozone = result_table.hourly.ozone[i]
                                lki = calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                -- Update totallkiValues with current LKI value
                                totallkiValues =  totallkiValues + calculatedLKI( ozone, pm10, pm2_5, nitrogen_dioxide )
                                dz.log( 'Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - LKI = ' ..lki  .. '; pm10 = ' ..  pm10 .. '; pm2_5 = ' ..  pm2_5 .. '; nitrogen_dioxide = ' ..  nitrogen_dioxide ..'; ozone = ' ..  ozone, dz.LOG_DEBUG )
                                intloopCounter = intloopCounter + 1
                            end
                        end
                        -- Calculate and store the average value for future hours in global_data.
                        --NB. This will be used in script d-StookwijzerLokaal
                        -- Correct intloopCounter for last round
                        intloopCounter = intloopCounter -1
                        _d.avgForcastLKI = _u.round( totallkiValues / intloopCounter, 0 )
                        dz.log( 'Found average LKI = ' .. _d.avgForcastLKI .. ' in ' .. intloopCounter .. ' records.', dz.LOG_DEBUG )
                        -- Keep track of the number of hours that is received.
                        _d.forcastHours = intloopCounter
                    else
                        dz.log( 'No result_table found', dz.LOG_ERROR )
                    end
    			else
                    dz.log( 'Item or JSON - NOT OK', dz.LOG_ERROR )
    			end

                -- Get the data for Wind.
                -- Log what we do.
                dz.log( 'forcastDays is set to  ' .. forcastDays, dz.LOG_DEBUG )
                -- Retrieve the data
                dz.openURL({
                    url = 'https://api.open-meteo.com/v1/forecast?latitude=' .. lat .. '&longitude=' .. long .. '&current=temperature_2m,apparent_temperature,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=wind_speed_10m&wind_speed_unit=ms&timezone=auto&forecast_days=' .. forcastDays,
                    method = 'GET',
				    callback = 'meteowind'
                })
            end -- meteoair

            -- For WIND -----------------------------------------
            if triggeredItem.trigger == 'meteowind' then
                -- Check the response and process the data.
                if ( triggeredItem.ok and triggeredItem.isJSON ) then
                    dz.log( 'Item and JSON - OK', dz.LOG_DEBUG )
                    -- We have some result. Store in table.
                    local result_table = triggeredItem.json
                    if type( result_table ) == "table" then
                        dz.log( 'result_table: type = ' .. type( result_table ), dz.LOG_DEBUG )
                        -- Get the current values
                        --"temperature_2m": 10.2,
                        --"apparent_temperature": 6.2,
                        --"wind_speed_10m": 4.71,
                        --"wind_direction_10m": 197,
                        --"wind_gusts_10m": 16.5
                        local temperature = result_table.current.temperature_2m
                        local chill = result_table.current.apparent_temperature
                        local windSpeedMs = _u.round( result_table.current.wind_speed_10m, 1 )
                        local windDirection = result_table.current.wind_direction_10m
                        local windDirectionString = _h.getDirectionfromDegree( windDirection )
                        local windGust = result_table.current.wind_gusts_10m
                        dz.log( 'Current values - temperature = ' ..  temperature .. '; chill = ' ..  chill .. '; windSpeedMs = ' ..  windSpeedMs ..'; windDirection = ' ..  windDirection .. '; windDirectionString = ' ..  windDirectionString ..'; windGust = ' ..  windGust, dz.LOG_DEBUG )

                        -- LOG current windSpeedMs with the last windSpeedMs from Weerlive.
                        local weerliveWindSpeed = dz.devices( 1026 ).speedMs  -- Gets last stored value from WeerliveWind sensor.
                        _h.AppendStringToFile( dz, '/home/janpep/domoticz/scripts/WindLog.txt', currentTime .. '\t' .. weerliveWindSpeed .. '\t' .. windSpeedMs .. '\n' )

                        intloopCounter = 1
                        local tc = #result_table.hourly.time
                            dz.log( 'Forcasthours = ' .. _d.forcastHours, dz.LOG_DEBUG )
                            for i = 1, tc do
                                if result_table.hourly.time[i] > resultTime and intloopCounter <= _d.forcastHours then
                                    dz.log( 'Record = ' .. intloopCounter .. ' - time - value: ' .. result_table.hourly.time[i] .. ' - ' .. result_table.hourly.wind_speed_10m[i], dz.LOG_DEBUG )
                                    -- Value for the current hour.
                                    meteowindSpeed = result_table.hourly.wind_speed_10m[i]
                                    totalwindspeedValues = totalwindspeedValues + meteowindSpeed                          
                                    intloopCounter = intloopCounter + 1
                                end
                            end
                        -- Calculate and store the average value for future hours in global_data.
                        --NB. This will be used in script d-StookwijzerLokaal.
                        -- Correct intloopCounter for last round
                        intloopCounter = intloopCounter -1                    
                        _d.avgForcastwindspeed = _u.round( totalwindspeedValues / intloopCounter, 1 )
                        dz.log( 'Found average windspeed = ' .. _d.avgForcastwindspeed .. ' in ' .. intloopCounter .. ' records.', dz.LOG_DEBUG )

                        -- Now update our meteowinddevice, which triggers d-StookwijzerLokaal script to produce the advice.
                        dz.log( 'Update ' .. dz.devices( meteowind_idx ).name  .. '.', dz.LOG_DEBUG )
                        dz.devices( meteowind_idx ).updateWind( windDirection, windDirectionString, windSpeedMs, windGust, temperature, chill )                    
                        
                        --Finally update the setpoint if dynamic mode is enabled.
                        --if useDynamicSetpoint == 1 then
                        --    -- Update the setpoint dynamically.
                        --    dz.log( 'useDynamicSetpoint is ON. Update ' ..  dz.devices( setpoint_idx ).name .. ' to ' .. forcastHoursLimit .. '.', dz.LOG_DEBUG )
                        --    dz.devices( setpoint_idx ).updateSetPoint( forcastHoursLimit )
                        --end
                    else
                        dz.log( 'No result_table found', dz.LOG_ERROR )
                    end
    			else
                    dz.log( 'Item or JSON - NOT OK', dz.LOG_ERROR )
    			end
            end
        end
	end
}
-- That's All --------------------------------------------------
Used devices are the same as mentioned earlier and can also be seen in the scripts.
The use of global_data is the same as mentioned earlier.

And the result:
Despite the fact that several devices are being updated and a lot of comparisons and calculations are taking place, it is still quite fast. As can be seen in the picture, the alert changed within the same second that the setpoint was changed.
LocaleStookwijzer-JanPep-6.png
LocaleStookwijzer-JanPep-6.png (24.13 KiB) Viewed 1710 times
Last edited by janpep on Friday 14 February 2025 16:06, edited 2 times in total.
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

When you use two sources, the result can be a bit confusing. The main difference between them is usually the wind speed from the other source.
This can sometimes differ by a few meters per second at the same time.
Attachments
LocaleStookwijzer-JanPep-7.png
LocaleStookwijzer-JanPep-7.png (24.09 KiB) Viewed 1707 times
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

And I go from one idea to another. I once saw something posted here with a progress bar. I think it would be nice to put the development of the alert code color per hour in a footer bar below the forecast. A graphical trend per hour seems to me a nice addition to the average over the same period.
To give you an idea, here it is in a more primitive way for 25 hour forecast.
LocaleStookwijzer-JanPep-8.png
LocaleStookwijzer-JanPep-8.png (725 Bytes) Viewed 1679 times
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
janpep
Posts: 268
Joined: Thursday 14 March 2024 10:11
Target OS: Linux
Domoticz version: 2025.1
Location: Netherlands
Contact:

Re: Dutch local 'Stookwijzer'

Post by janpep »

Next step with optional colored footer (1 or 0) and configurable amount of hours (1-72).
Here in test example set to 40 hours ahead.
I am thinking of deleting the log file every now and then, because the amount of HTML just makes it bigger and there is not much point in keeping a log of this text anyway.
LocaleStookwijzer-JanPep-9.png
LocaleStookwijzer-JanPep-9.png (14.28 KiB) Viewed 1403 times
Dz on Ubuntu VM on DS718+ behind FRITZ!Box.
EvoHome; MELCloud; P1 meter; Z-Stick GEN5; Z-Wave-js-ui; Sonoff USB-Dongle Plus-E; Zigbee2Mqtt; MQTT; Greenwave powernodes 1+6; Fibaro switch, plugs, smoke; FRITZ!DECT 200. Scripts listed in profile interests.
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest