blinds move on the sun

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

Moderator: leecollings

hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

blinds move on the sun

Post by hestia »

I have a sunshade (brise-soleil, on the roof of my veranda, see picture after) and a blind (vertical).
I've made a script that move automatically the sunshades, blinds, shutters (in the following, I use blinds in all cases), depending on:
- the outside temperature,
- the position of the sun and therefore also "day" and "night",
- the time of day.

There are 2 modes: Automatic and Manual
- Manual is the mode of the normal button, taken into account to know if you are in automatic or not
- Automatic: the blind moves by itself whatever the time and the season

Automatic, there are 3 programs that change automatically
- Cold: below a certain outside temperature: you close the blind at night to keep the heat and you open it during the day to see and receive the heat of the sun; I set 20°C
- Hot: when there is a lot of sun, we open the blinds at night to cool down and during the day we follow the sun to close the blinds as the sun rises (for the sunshades, because for a blind without slats, it's all or nothing).
I took the outside temperature as a criterion while waiting to be able to measure the solar radiation with a good reactivity; I put 24°C (but it depends on where is the thermometer; in my case, it is very in the shade)
- Warm: neither Cold, nor Hot: we leave the blind open all the time
Here is the script

Code: Select all

--[[
A dz dzVents Script to close or open blinds or shutters regarding outside information: light, temperature, solar posiiton and time
In this script, it could be vertical shutters or blinds like roller shades (w/o slates) or fix shutters or blinds with slates ("brise-soleil")
For brise-soleil, the slates follow the sun to get just the shade needed
In the following I'm going to use the word blind only for all cases
It is possible to declare several blinds in the same script with a different configuration for each ones

tips to setup the selector (BLIND_ID)
10: &#9650  => UP
20: &#9724  => STOP
30: &#9660  => DOWN
40: A       => Auto

05/04/2020 - new!
07/02/2021 - clarify log if newBlindProgram is empty
04/04/2021 - add new type of device for SUN_CRITERIA (temperature, lux, percentage)
04/06/2021 - round angle in the device name / fix lastSlatAngle init / fix rename conditions
06/07/2021 - logging precision
08/08/2021 - bug fixed, changed lastBlindProgram to newBlindProgram (one occurence forgotten in Auto mode)
11/08/2021 - clarify log
17/08/2021 - the sun radiation (lux or W/m2) is considered on the floor (horizontal projection of the sun radiation or measured on an horizontal surface)
                for a whole house what matter is more the whole radiation than only the horizontal one
                particularly when the sun is low and vertical windows are considered
                to have a better criteria for the impact of the sun on a house, the whole value is used (projection on the floor removed)
19/05/2022 - adjust the rounded of the movement to avoid a line of sun when the brise-soleil are closing
22/05/2022 - separate sun radiation and max outside temperature to combine them
06/06/2022 - fix bugs where blinds are mostly on east and west => refactoring some old parts done for vertical windows using new parts (see 17/08/2021)
                Breaking change BLIND_AZIMUTH is now the Azimuth of the normal of the area
12/06/2022 - fix "nil value (local 'P_SlatAngleTarget'"
14/06/2022 - idem
17/06/2022 - remove cosIncidence from the sunRadiationValue: thru a glass at any inclination, the sun strike a body the same?
            
next?      - add HOT+ program: shut the blinds if very hot and the sun strikes the blind


prerequisite: dzVents Version: 3.0.2
3 sensors: 
SOLAR_ALTITUDE  Device that gives Solar Altitude see https://www.domoticz.com/wiki/index.php?title=Lua_dzVents_-_Solar_Data:_Azimuth_Altitude_Lux
SOLAR_AZIMUTH   Device that gives Solar Azimuth see https://www.domoticz.com/wiki/index.php?title=Lua_dzVents_-_Solar_Data:_Azimuth_Altitude_Lux
OUTSIDE_TEMP    Device that gives Outside temperature
SUN_RADIATION   1 or 2 devices that gives the value of the outside radiation to determine if the blind must be closed because it is too sunny
    (lux or W/m2)
SUN_RADIATION: 1 or 2 devices to determine if the blind must be closed (according to the sun position) because it is too sunny
It could be solar radiation in lux, or solar radiation in W/m2
There could be 1 or 2 devices
If 1 device, it is a total radiation value (usually on the ground)
If 2 devices, the SUN_RADIATION1 is the direct radiation and SUN_RADIATION2 is the scattered (or diffuse) radiation
and the total radiation is calculated on the surface of the blind with the angle of this surface and the sun
    
At the first use or after any change of the script name, to click Auto mode to initiate "data"
--]]

-- log options
local LOG_DEBUG = 0         -- 0 =>ERROR / 1 => FORCE / 2 => DEBUG
local LOG_LEVEL
local LOGGING
if LOG_DEBUG == 2 then
    LOGGING = domoticz.LOG_DEBUG
    LOG_LEVEL = domoticz.LOG_DEBUG
elseif LOG_DEBUG == 1 then
    LOGGING = domoticz.LOG_FORCE
    LOG_LEVEL = domoticz.LOG_FORCE
else
    LOGGING = domoticz.LOG_ERROR
    LOG_LEVEL = domoticz.LOG_INFO
end


local ALL_MAX_SUN_RADIATION = 400
local ALL_MAX_OUTSIDE_TEMP = 21
local ALL_XXL_OUTSIDE_TEMP = 30 -- future version...
local ALL_MIN_OUTSIDE_TEMP = 20

local BLINDS = 
    {   [1203] =                            -- Dummy device with Automatics command (to create) (id or 'name')
            {
            ['BLIND_TYPE'] = 'V',           -- "B" Brise Soleil or "V" Vertical
            ['BLIND_NAME'] = 'Store',-- Name of the dummy device
            ['BLIND_ID'] = 953,             -- Device of the BLIND to close or open (id or 'name') 
            ['BLIND_AZIMUTH'] = 289,        -- Angle from the projection on the ground of the normal of the blind and the North (Azimuth of the normal of the area)
            ['SOLAR_ALTITUDE_MIN'] = 10,    -- Solar Altitude where the sun is hidden by a house, a tree, if nothing 0    
            ['MAX_SUN_RADIATION'] = ALL_MAX_SUN_RADIATION,   -- If radiation is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MAX_OUTSIDE_TEMP'] = ALL_MAX_OUTSIDE_TEMP,   -- If temperature is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MIN_OUTSIDE_TEMP']  = ALL_MIN_OUTSIDE_TEMP,     -- If outside temperature in less than this min temp, the blind is closed night (COLD) 
            ['SLAT_DOWN_ANGLE']  = -90,     -- Angle from the horizontal when the slat is down (<0), -90 for vertical blind
            ['SLAT_UP_ANGLE'] = 90,         -- Angle from the horizontal when the slat is up, 90 for vertical blind
            ['SLAT_LENGHT']  = '',          -- Length of the slate, any unit (cm...), nil for vertical blind
            ['SLAT_DISTANCE']  = '',        -- Distance between slates, any unit (cm...), nil for vertical blind
            ['BLIND_ANGLE']  = 90,          -- Angle of the blind from the horizontal (>0)
            ['TTC_SLAT_SEC'] = 1            -- Time To Close the blind in seconds, to be measured, 1 for vertical blind
            },
        [1122] =                             -- Dummy device with Automatics command (to create) (id or 'name')
            {
            ['BLIND_TYPE'] = 'B',           -- "B" Brise Soleil or "V" Vertical
            ['BLIND_NAME'] = 'Brise Soleil',-- Name of the dummy device
            ['BLIND_ID'] = 673,             -- Device of the BLIND to close or open (id or 'name') 
            ['BLIND_AZIMUTH'] = 289,        -- Angle from the projection on the ground of the normal of the blind and the North (Azimuth of the normal of the area)
            ['SOLAR_ALTITUDE_MIN'] = 15,   -- 20 pour relever les lattes -- Solar Altitude where the sun is hidden by a house, a tree, if nothing 0    
            ['MAX_SUN_RADIATION'] = ALL_MAX_SUN_RADIATION,   -- If radiation is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MAX_OUTSIDE_TEMP'] = ALL_MAX_OUTSIDE_TEMP,   -- If temperature is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MIN_OUTSIDE_TEMP']  = ALL_MIN_OUTSIDE_TEMP,     -- If outside temperature in less than this min temp, the blind is closed night (COLD) 
            ['SLAT_DOWN_ANGLE']  = -4,      -- Angle from the horizontal when the slat is down (<0), -90 for vertical blind
            ['SLAT_UP_ANGLE'] = 55,         -- Angle from the horizontal when the slat is up, 90 for vertical blind
            ['SLAT_LENGHT']  = 8,           -- Length of the slate, any unit (cm...), nil for vertical blind
            ['SLAT_DISTANCE']  = 7,         -- Distance between slates, any unit (cm...), nil for vertical blind
            ['BLIND_ANGLE']  = 16,          -- Angle of the blind from the horizontal (>0)
            ['TTC_SLAT_SEC'] = 16           -- Time To Close the blind in seconds, to be measured, 1 for vertical blind
            }
    }
--[[            ,
        [2618] =                             -- Dummy device with Automatics command (to create) (id or 'name')
            {
            ['BLIND_TYPE'] = 'V',           -- "B" Brise Soleil or "V" Vertical
            ['BLIND_NAME'] = 'Rideau Studio',-- Name of the dummy device
            ['BLIND_ID'] = 205,             -- Device of the BLIND to close or open (id or 'name') 
            ['BLIND_AZIMUTH'] = 19,         -- Angle from the projection on the ground of the normal of the blind and the North (Azimuth of the normal of the area)
            ['SOLAR_ALTITUDE_MIN'] = 10,    -- Solar Altitude where the sun is hidden by a house, a tree, if nothing 0    
            ['MAX_SUN_RADIATION'] = 200,   -- If radiation is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MAX_OUTSIDE_TEMP'] = 10,   -- If temperature is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MIN_OUTSIDE_TEMP']  = 10,     -- If outside temperature in less than this min temp, the blind is closed night (COLD) 
            ['SLAT_DOWN_ANGLE']  = -90,     -- Angle from the horizontal when the slat is down (<0), -90 for vertical blind
            ['SLAT_UP_ANGLE'] = 90,         -- Angle from the horizontal when the slat is up, 90 for vertical blind
            ['SLAT_LENGHT']  = '',          -- Length of the slate, any unit (cm...), nil for vertical blind
            ['SLAT_DISTANCE']  = '',        -- Distance between slates, any unit (cm...), nil for vertical blind
            ['BLIND_ANGLE']  = 90,          -- Angle of the blind from the horizontal (>0)
            ['TTC_SLAT_SEC'] = 1            -- Time To Close the blind in seconds, to be measured, 1 for vertical blind
            },
        [2619] =                             -- Dummy device with Automatics command (to create) (id or 'name')
            {
            ['BLIND_TYPE'] = 'V',           -- "B" Brise Soleil or "V" Vertical
            ['BLIND_NAME'] = 'Rideau Chambre',-- Name of the dummy device
            ['BLIND_ID'] = 206,             -- Device of the BLIND to close or open (id or 'name') 
            ['BLIND_AZIMUTH'] = 199,        -- Angle from the projection on the ground of the normal of the blind and the North (Azimuth of the normal of the area
            ['SOLAR_ALTITUDE_MIN'] = 10,    -- Solar Altitude where the sun is hidden by a house, a tree, if nothing 0    
            ['MAX_SUN_RADIATION'] = 200,   -- If radiation is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MAX_OUTSIDE_TEMP'] = 10,   -- If temperature is more than this max, the blind is closed during the day and according to the sun position (HOT)
            ['MIN_OUTSIDE_TEMP']  = 10,     -- If outside temperature in less than this min temp, the blind is closed night (COLD) 
            ['SLAT_DOWN_ANGLE']  = -90,     -- Angle from the horizontal when the slat is down (<0), -90 for vertical blind
            ['SLAT_UP_ANGLE'] = 90,         -- Angle from the horizontal when the slat is up, 90 for vertical blind
            ['SLAT_LENGHT']  = '',          -- Length of the slate, any unit (cm...), nil for vertical blind
            ['SLAT_DISTANCE']  = '',        -- Distance between slates, any unit (cm...), nil for vertical blind
            ['BLIND_ANGLE']  = 90,          -- Angle of the blind from the horizontal (>0)
            ['TTC_SLAT_SEC'] = 1            -- Time To Close the blind in seconds, to be measured, 1 for vertical blind
            }
    }
--]]

local TEST = 203  -- a dummy switch for testing w/o waiting minutes / remove comment to use / comment to ignore

local SOLAR_ALTITUDE = 338      -- Device that give Solar Altitude
local SOLAR_AZIMUTH = 337       -- Device that give Solar Azimuth

local OUTSIDE_TEMP = 2560        -- Device that give Outside temperature
local OUTSIDE_TEMP_MARGING = .3 -- Marging to have an hysteresis to change mode or program to avoid multiple changes when few changes of temp

local SUN_RADIATION1 = 1914 -- total radiation if 1 device, direct radiation if 2 devices
local SUN_RADIATION2 = 1915 -- if the first is the direct radiation, this one is the scattered radiation
local SUN_RADIATION_MARGING = 20 -- Marging to have an hysteresis to change mode or program to avoid multiple changes when few changes of the value

local DEVICES_TRIGGER = {
        OUTSIDE_TEMP, 
        SUN_RADIATION1,
        TEST
    } -- SOLAR_AZIMUTH is updated at the same time as SUN_RADIATION1

-- ... next add the BLINDS definitions
for blindId, _ in pairs(BLINDS) do
    table.insert( DEVICES_TRIGGER, blindId )
end

--local DEVICES_TRIGGER = {1203, 1122, 2618, 2619, OUTSIDE_TEMP, SUN_RADIATION1, TEST} -- SOLAR_AZIMUTH is updated at the same time as SUN_RADIATION

--local TIME_INTERVAL = 'every 5 minutes'


return {
    logging =   {
                level =   LOGGING
                },
    on      =   {   devices =   DEVICES_TRIGGER,
                    --timer   =   {TIME_INTERVAL} -- trigger to choose
        },
                        
    data    =   {   slatAngle =         { initial = {}},    -- Last angle of the slate
                    lastMaxSlatAngle =  { initial = {}},    -- Last max angle of the slate to kwnow if it goes up or down -- 19/05/2022
                    blindMode =         { initial = {}},    -- Last mode of the blind Manual, Auto
                    blindProgram =      { initial = {}}     -- Last program of the blind Cold, Warm, Hot
        },
        
	execute = function(dz, item, triggerInfo)
        
        _G.logMarker =  dz.moduleLabel -- set logmarker to scriptname 
    	local _u =  dz.utils

    -- /// Functions start \\\

    local function logWrite(str,level)  -- Support function for shorthand debug log statements
        if level == nil then
            level = LOG_LEVEL
        end
        dz.log(tostring(str),level)
    end
    
        
    local function CalculateSunIncidence(P_SolarAzimuth, P_SolarAltitude, P_AreaAzimuth, P_AreaInclination)
        --logWrite('CalculateSunIncidence')
        -- Calculate the sun incidence (the cosinuss) on a tilt surface

        --P_SolarAzimuth: azimuth of the sun in degrees (from the Noth)
        --P_SolarAltitude: elevation of the sun or altitude in degrees
        --P_AreaAzimuth: azimuth of the normale surface in degrees (from the Noth)
        --P_AreaInclination: inclination of the surface (from the horizontal)

        logWrite('P_SolarAltitude: ' .. P_SolarAltitude)
        logWrite('P_SolarAzimuth: ' .. P_SolarAzimuth)
        logWrite('P_AreaInclination: ' .. P_AreaInclination)
        logWrite('P_AreaAzimuth: ' .. P_AreaAzimuth)
            
        local F_cosIncidence
        F_cosIncidence = math.cos(math.rad(P_SolarAltitude)) * math.sin(math.rad(P_AreaInclination)) * math.cos(math.rad(P_AreaAzimuth - P_SolarAzimuth))
                    + math.sin(math.rad(P_SolarAltitude)) * math.cos(math.rad(P_AreaInclination))
        F_cosIncidence = _u.round(F_cosIncidence, 3)
        
        --logWrite('F_cosIncidence: ' .. F_cosIncidence)
        local F_Incidence = math.deg(math.acos(F_cosIncidence))
        --logWrite('F_Incidence: ' .. F_Incidence)
        
        if F_cosIncidence < 0 then
            logWrite('The sun is behind, cos incidence=' .. F_cosIncidence)
            F_cosIncidence = 0
        end

        logWrite('SolarAltitude: ' .. P_SolarAltitude .. ' SolarAzimuth: ' .. P_SolarAzimuth
            .. ' AreaInclination: ' .. P_AreaInclination .. ' AreaAzimuth: ' .. P_AreaAzimuth
            .. ' CosIncidence: ' .. F_cosIncidence .. ' ' .. ' Incidence: ' .. _u.round(F_Incidence,0) .. '°')
        return F_cosIncidence
    end
    

    local function GetSunRadiation(P_SolarAzimuth, P_SolarAltitude, P_AreaAzimuth, P_AreaInclination)
        -- Get the sun criteria values
        local F_dev_sunRadiation = dz.devices(SUN_RADIATION1)
        local F_sunRadiationValue
        --logWrite('sunRadiationValue ' .. F_dev_sunRadiation.name .. ' type ' .. F_dev_sunRadiation.deviceType .. ' s/type ' .. F_dev_sunRadiation.deviceSubType)
   
        local F_cosIncidence = CalculateSunIncidence(P_SolarAzimuth, P_SolarAltitude, P_AreaAzimuth, P_AreaInclination)
            
        if F_dev_sunRadiation.deviceSubType == 'Lux' then
            F_sunRadiationValue = F_dev_sunRadiation.lux
            
        elseif F_dev_sunRadiation.deviceSubType == 'Solar Radiation' then    
            local F_solarRadiation = F_dev_sunRadiation.radiation
            if SUN_RADIATION2 ~= nil then -- it is the scattered radiation and the previous was the direct
                local F_solarRadiationScattered = dz.devices(SUN_RADIATION2).radiation
                -- the total radation on the blind depends on the incidence of the sun
                -- F_cosIncidence = CalculateSunIncidence(P_SolarAzimuth, P_SolarAltitude, P_AreaAzimuth, P_AreaInclination) -- to remove 06/06/2022
                -- the direct radiation is projected on the incidence, the scattered (diffused) is in all directions
                
                --F_sunRadiationValue = math.max(_u.round(F_solarRadiation * F_cosIncidence + F_solarRadiationScattered, 0), 0)

                F_sunRadiationValue = math.max(_u.round(F_solarRadiation + F_solarRadiationScattered, 0), 0) -- 17/06/2022 -- thru a glass at any inclinaison, the sun strike a body the same?
            
            else -- it is the total radiation
                F_sunRadiationValue = F_solarRadiation
            end
            
        else
            logWrite(F_dev_sunRadiation.id .. ' ' .. F_dev_sunRadiation.name .. ' device Type not supported ' .. F_dev_sunRadiation.deviceType, dz.LOG_ERROR)
        end

        logWrite('sunRadiation ' .. F_sunRadiationValue, dz.LOG_DEBUG)
    
    return F_sunRadiationValue, F_cosIncidence
    end
 
    local function InitPosition(P_devBlind, P_SLAT_UP_ANGLE)
        P_devBlind.open().silent()
        return P_SLAT_UP_ANGLE
    end
    
    
    local function CalculateSlatAngle(P_SolarAzimuth, P_SolarAltitude, P_BlindAzimuth, P_SlatDownAngle, P_SlatUpAngle, P_SolarAltitudeMin, P_SLAT_LENGHT, P_SLAT_DISTANCE, P_BLIND_ANGLE, P_BLIND_TYPE, P_cosIncidence)
        -- calculate the angle the slat need to have to be against the sun
        -- normal to the projection of the sun ray on a plan perpendicular at the blind azimuth
        --
        logWrite('CalculateSlatAngle')
        logWrite('P_BLIND_TYPE ' .. P_BLIND_TYPE)
        logWrite('P_SolarAzimuth ' .. P_SolarAzimuth)
        logWrite('P_SolarAltitude ' .. P_SolarAltitude)
        logWrite('P_BlindAzimuth ' .. P_BlindAzimuth)
        logWrite('P_SolarAltitudeMin ' .. P_SolarAltitudeMin)
        logWrite('P_cosIncidence '  .. P_cosIncidence)
        --
        --local F_SunInTheFront = false
        local F_SlatAngle
        if P_SolarAltitude < P_SolarAltitudeMin then -- if the sun is low, open the slates
            F_SlatAngle = P_SlatUpAngle
            logWrite('F_SlatAnglee open as the sun is low :' .. F_SlatAngle, dz.LOG_FORCE)
        else
            if P_cosIncidence > 0 then 
                logWrite('Sun on the area', dz.LOG_FORCE)

                if P_BLIND_TYPE == "V" then -- vertical blind: open or closed
                    --if F_SunInTheFront then -- the slate should be closed enough not to let the sun shine in
                       F_SlatAngle = P_SlatDownAngle
                       logWrite('Blind closed: ' .. F_SlatAngle, dz.LOG_FORCE)
                    --else
                        --F_SlatAngle = P_SlatUpAngle
                        --logWrite('Blind open: ' .. F_SlatAngle)
                    --end
    
                elseif P_BLIND_TYPE == "B" then -- brise-soleil, the angle of slates is calculated to follow the sun
                
                
                    local F_IncidenceSunBlind = P_BlindAzimuth - P_SolarAzimuth -- angle of the sun on the front of the blind
                    local F_ProjectedSolarAltitude = math.deg(math.atan
        		                            (
    		                                math.tan(math.rad(P_SolarAltitude))
    		                                /math.cos(math.rad(F_IncidenceSunBlind))
    	                                    )    
                                                            )

                    if F_ProjectedSolarAltitude > 0 then
                        logWrite('F_ProjectedSolarAltitude: ' .. F_ProjectedSolarAltitude)
                        logWrite('P_SlatDownAngle ' .. P_SlatDownAngle .. ' / P_SlatUpAngle ' .. P_SlatUpAngle)
                        local q =  math.tan(math.rad(F_ProjectedSolarAltitude + P_BLIND_ANGLE))
                        logWrite('q=' .. q)
                        --print('q ' .. q)
                        local a = (P_SLAT_DISTANCE / P_SLAT_LENGHT) + 1
                        --print('a '  .. a)
                        local b = 2 / q
                        --print('b ' .. b)
                        local c = (P_SLAT_DISTANCE / P_SLAT_LENGHT) - 1
                        --print('c ' .. c)
                        local d = b*b - 4*a*c
                        --print('d ' .. d)
                        local rac2d = math.sqrt(d)
                        --print('rac2d ' .. rac2d)
                        local t = (rac2d - b) / (2*a)
                        --print('t ' .. t)
                        local i =  2*math.deg(math.atan(t)) -- angle between the slat and the blind (roof)
                        --print('i ' .. i)
                        F_SlatAngle = i - P_BLIND_ANGLE
                        logWrite('F_SlatAngle therorical: ' .. F_SlatAngle)
        
                        -- the slate angle should be inside the capability of the blind
                        if F_SlatAngle > P_SlatUpAngle then
                            F_SlatAngle = P_SlatUpAngle
                        elseif F_SlatAngle < P_SlatDownAngle then
                            F_SlatAngle = P_SlatDownAngle
                        end
                        logWrite('Slat angle target: ' .. F_SlatAngle .. '° / Inclination from horizontal: ' .. (F_SlatAngle - P_BLIND_ANGLE) .. '°', dz.LOG_FORCE)
                    else
                        logWrite('F_ProjectedSolarAltitude: ' .. F_ProjectedSolarAltitude .. ' < 0 ; sun not on the side of the slats', dz.LOG_FORCE)
                        F_SlatAngle = P_SlatUpAngle -- 14/06/2022
                    end
                    
                else
                    logWrite("BLIND_TYPE unknown " .. P_BLIND_TYPE, dz.LOG_ERROR)
                    return(0)
                end
            else
                logWrite('Sun not on the area', dz.LOG_FORCE)     
                F_SlatAngle = P_SlatUpAngle -- 12/06/2022
            end

        end
        return(_u.round(F_SlatAngle,0))
    end
    
    
    local function CalculateSlatMvt(P_SlatAngleTarget, P_LastSlatAngle, P_lastMaxSlatAngle, P_SLAT_UP_ANGLE, P_SLAT_DOWN_ANGLE, P_SLAT_MARGING, P_TTC_SLAT_SEC)
    -- calculte the time is seconds to move the slates and the direction
        --[[
        logWrite('CalculateSlatMvt')
        logWrite('P_SlatAngleTarget '.. P_SlatAngleTarget )
        logWrite('P_LastSlatAngle ' .. P_LastSlatAngle )
        --]]
        -- if the slat target is near an edge it is moving to this edge
        local F_SlatMvt -- duration of the move in seconds
        local F_SlatAngleDelta
        if P_SlatAngleTarget == P_SLAT_UP_ANGLE and P_LastSlatAngle == P_SLAT_UP_ANGLE then -- already up
            F_SlatMvt = 0
            F_SlatAngleDelta = 0
            logWrite('P_SlatAngleTarget already to the max => F_SlatMvt ' .. F_SlatMvt)
        elseif P_SlatAngleTarget + P_SLAT_MARGING > P_SLAT_UP_ANGLE then -- up edge
            F_SlatMvt = 999
            F_SlatAngleDelta = 999
            logWrite('P_SlatAngleTarget near the up edge ' .. F_SlatMvt)
        elseif P_SlatAngleTarget == P_SLAT_DOWN_ANGLE and P_LastSlatAngle == P_SLAT_DOWN_ANGLE then -- already down
            F_SlatMvt = 0
            F_SlatAngleDelta = 0
            logWrite('P_SlatAngleTarget already to the min ' .. F_SlatMvt)
        elseif P_SlatAngleTarget - P_SLAT_MARGING <= P_SLAT_DOWN_ANGLE then -- down edge
            F_SlatMvt = -999
            F_SlatAngleDelta = -999
            logWrite('P_SlatAngleTarget near the down edge ' .. F_SlatMvt, dz.LOG_FORCE)
        else
            F_SlatAngleDelta = P_SlatAngleTarget - P_LastSlatAngle
            logWrite('=> P_SlatAngleTarget ' .. P_SlatAngleTarget, dz.LOG_FORCE)
            logWrite('P_LastSlatAngle ' .. P_LastSlatAngle, dz.LOG_FORCE)
            logWrite('F_SlatAngleDelta ' .. F_SlatAngleDelta, dz.LOG_FORCE)
            F_SlatMvt = P_TTC_SLAT_SEC * F_SlatAngleDelta / (P_SLAT_UP_ANGLE - P_SLAT_DOWN_ANGLE)
            logWrite('F_SlatMvt ' .. F_SlatMvt .. ' s', dz.LOG_FORCE)
            logWrite('=> P_lastMaxSlatAngle: ' .. P_lastMaxSlatAngle, dz.LOG_FORCE)
            if P_lastMaxSlatAngle == nil or P_SlatAngleTarget < P_lastMaxSlatAngle then -- 19/05/2022
                logWrite('=> The slates go down', dz.LOG_FORCE)
                F_SlatMvt = math.ceil(F_SlatMvt)
                --F_SlatMvt = math.floor(F_SlatMvt)
            else
                F_SlatMvt = math.floor(F_SlatMvt)
                --F_SlatMvt = math.ceil(F_SlatMvt)
                logWrite('The slates go up', dz.LOG_FORCE)
            end
            
            -- rounded toward the next move
            --[[
            if F_SlatMvt < 0 then
                F_SlatMvt = math.floor(F_SlatMvt)
            else
                F_SlatMvt = math.ceil(F_SlatMvt)
            end
            --]]
            F_SlatMvt = _u.round(F_SlatMvt)
            logWrite('F_SlatMvt ' .. F_SlatMvt .. ' s', dz.LOG_FORCE)
            F_SlatAngleDelta = F_SlatMvt * (P_SLAT_UP_ANGLE - P_SLAT_DOWN_ANGLE) / P_TTC_SLAT_SEC -- the "real" move
            logWrite('F_SlatAngleDelta ' .. F_SlatAngleDelta, dz.LOG_FORCE)
        end
    
        logWrite('F_SlatMvt ' .. F_SlatMvt)

        logWrite('F_SlatAngleDelta ' .. F_SlatAngleDelta)
        return F_SlatMvt, F_SlatAngleDelta
    end
    
    
    local function MoveSlat(P_SlatMvt, P_devBlind)
    -- move the slat the number of seconds in parameter, if > 0 to the open direction , if < 0 to the close direction
        logWrite('MoveSlate ' .. P_SlatMvt .. ' ' .. P_devBlind.name)
        if P_SlatMvt == 0 then
            logWrite('No need to move the blind ' .. P_SlatMvt .. ' sec')
        else
            if P_SlatMvt > 0 then
                --if P_SlatMvt < 999 then
                    --P_devBlind.open().forSec(P_SlatMvt).silent()
                    logWrite('Blind opens for ' .. P_SlatMvt .. ' seconds')
                --else
                    P_devBlind.open().silent()
                    --logWrite('Blind opens to the max')
                --end
            else
                --if P_SlatMvt > -999 then
                    logWrite('Blind closes for ' .. P_SlatMvt .. ' seconds')
                --else
                    P_devBlind.close().silent()
                    --logWrite('Blind closes ')
                --end
                P_SlatMvt = - P_SlatMvt 
                logWrite('Blind closes ' .. P_SlatMvt .. ' seconds')
            end
            
            if math.abs(P_SlatMvt) ~= 999 then -- it is not an edge position
                P_devBlind.stop().afterSec(P_SlatMvt).silent()
            end
        end
        return
    end
    
    
    local function Calibration(P_timeSec, P_devBlind)
        logWrite('CALIBRATION ' .. P_timeSec, dz.LOG_FORCE)
        MoveSlat(P_timeSec, P_devBlind)
        currentBlindMode = 'CALIBRATION'
        return
    end
    
	-- \\\ Functions end ///

        if item.isDevice then -- selector by sbdy or change is temp or sun criteria or sun position
            logWrite('==>> triggered by device ' ..  item.id .. ' ' .. item.name .. ' ' .. item.state, dz.LOG_FORCE)
        elseif item.isTimer then -- Timer trigger
            logWrite('==>> triggered by timer', dz.LOG_FORCE)
        else -- Impossible error!
            logWrite('==>> triggered by ?????', dz.LOG_ERROR)
            return
        end

    
        for blindId, blinfInfo in pairs(BLINDS) do
            local devDummyBlind = dz.devices(blindId)               -- Switch dummy device with commands 
            local devBlind = dz.devices(BLINDS[blindId].BLIND_ID)   -- Device of the BLIND to close or open

            logWrite('-->> ' .. devDummyBlind.name .. ' ' .. blindId)
            logWrite(devBlind.name .. ' ' .. devBlind.id)
            
            local lastSlatAngle = dz.data.slatAngle[blindId]
            local lastMaxSlatAngle = dz.data.lastMaxSlatAngle[blindId]
            local currentBlindMode = dz.data.blindMode[blindId]
            local lastBlindProgram = dz.data.blindProgram[blindId]
            
            if currentBlindMode == nil then currentBlindMode = 'INITIAL' end
            if lastBlindProgram == nil then lastBlindProgram = 'INITIAL' end
            
            if lastSlatAngle == nil then
                if currentBlindMode == 'Auto' then -- 01/06/2021
                    logWrite('Start ' .. currentBlindMode .. ' lastSlatAngle null !!!', dz.LOG_ERROR)
                else
                    logWrite('Start ' .. currentBlindMode .. ' slat angle null')
                end
            else
                logWrite('Start ' .. currentBlindMode .. ' slat angle ' .. lastSlatAngle)
            end
                
            local BLIND_TYPE = BLINDS[blindId].BLIND_TYPE
            local BLIND_NAME = BLINDS[blindId].BLIND_NAME
            local BLIND_AZIMUTH = BLINDS[blindId].BLIND_AZIMUTH
            local SOLAR_ALTITUDE_MIN = BLINDS[blindId].SOLAR_ALTITUDE_MIN
            local SLAT_DOWN_ANGLE = BLINDS[blindId].SLAT_DOWN_ANGLE
            local SLAT_UP_ANGLE = BLINDS[blindId].SLAT_UP_ANGLE
            local SLAT_LENGHT = BLINDS[blindId].SLAT_LENGHT
            local SLAT_DISTANCE = BLINDS[blindId].SLAT_DISTANCE
            local BLIND_ANGLE = BLINDS[blindId].BLIND_ANGLE


            -- < Device trigger
            if item.isDevice then -- selector by sbdy or change is temp or sun criteria or sun position
                if item == devDummyBlind then -- sbdy used the dummy selector
                    if devDummyBlind.level == 10 then -- up
                        if CALIBRATION ~= nil then -- move the slate up to count the times to open
                            Calibration(CALIBRATION, devBlind)
                            return
                        else
                            devBlind.open().silent()
                            lastSlatAngle =  SLAT_UP_ANGLE
                            lastMaxSlatAngle = SLAT_UP_ANGLE
                            currentBlindMode = 'Manual'
                            dz.log('devDummyBlind level: ' .. devDummyBlind.level .. ' open', LOG_LEVEL)
                        end
                    elseif devDummyBlind.level == 20 then -- stop
                        devBlind.stop().silent()
                        lastSlatAngle =  nil
                        currentBlindMode = 'Manual'
                        logWrite('devDummyBlind level: ' .. devDummyBlind.level .. ' stop')
                    elseif devDummyBlind.level == 30 then -- down                    
                        if CALIBRATION ~= nil then -- move the slate up to count the times to close
                            Calibration(-CALIBRATION, devBlind)
                            return
                        else
                            devBlind.close().silent()
                            devBlind.close().afterSec(3).silent() -- twice in case of loss of message (12/11/2020)
                            lastSlatAngle =  SLAT_DOWN_ANGLE
                            currentBlindMode = 'Manual'
                            logWrite('devDummyBlind level: ' .. devDummyBlind.level .. ' close')
                        end
                    elseif devDummyBlind.level == 40 then -- automatic
                        if currentBlindMode ~= 'Auto' then
                            lastSlatAngle = nil -- to force an init in Auto mode
                            logWrite('devDummyBlind level: ' .. devDummyBlind.level .. ' init auto mode')
                        else
                            logWrite('devDummyBlind level: ' .. devDummyBlind.level .. ' go on auto mode')
                        end
                        currentBlindMode = 'Auto'
                    else
                        logWrite('devDummyBlind level unknown: ' .. devDummyBlind.state, dz.LOG_ERROR)
                    end
                end
            end
       
            
            -- < Auto mode
            local MAX_SUN_RADIATION = BLINDS[blindId].MAX_SUN_RADIATION
            logWrite('MAX_SUN_RADIATION: ' .. MAX_SUN_RADIATION, dz.LOG_DEBUG)
            local MAX_OUTSIDE_TEMP =  BLINDS[blindId].MAX_OUTSIDE_TEMP
            logWrite('MAX_OUTSIDE_TEMP: ' .. MAX_OUTSIDE_TEMP, dz.LOG_DEBUG)
            local MIN_OUTSIDE_TEMP =  BLINDS[blindId].MIN_OUTSIDE_TEMP
            logWrite('MIN_OUTSIDE_TEMP: ' .. MIN_OUTSIDE_TEMP, dz.LOG_DEBUG)
            logWrite('SLAT_UP_ANGLE: ' .. BLINDS[blindId].SLAT_UP_ANGLE, dz.LOG_DEBUG)
            logWrite('SLAT_DOWN_ANGLE: ' .. BLINDS[blindId].SLAT_DOWN_ANGLE, dz.LOG_DEBUG)
            local TTC_SLAT_SEC = BLINDS[blindId].TTC_SLAT_SEC
            
            local SLAT_MARGING = (BLINDS[blindId].SLAT_UP_ANGLE - BLINDS[blindId].SLAT_DOWN_ANGLE) / BLINDS[blindId].TTC_SLAT_SEC  -- choice: it is 1 second = 1 move
            --logWrite('SLAT_MARGING ' .. SLAT_MARGING, dz.LOG_FORCE)
            
            local outsideTemp = dz.devices(OUTSIDE_TEMP).temperature
            local sunAzimuth = tonumber(dz.devices(SOLAR_AZIMUTH).sValue)
            logWrite('sunAzimuth: ' .. sunAzimuth, dz.LOG_DEBUG)
            local sunAltitude = tonumber(dz.devices(SOLAR_ALTITUDE).sValue)
            logWrite('sunAltitude: ' .. sunAltitude, dz.LOG_DEBUG)
            local wBlindNormalAzimuth

            local sunRadiationValue, cosIncidence = GetSunRadiation(sunAzimuth, sunAltitude, BLIND_AZIMUTH, BLIND_ANGLE) 
            
            local newBlindProgram
            local wCold = false
            local wHot = false
            
            logWrite('lastBlindProgram ' .. lastBlindProgram .. ' sunRadiationValue ' .. sunRadiationValue .. ' outsideTemp ' .. outsideTemp .. ' cosIncidence ' .. cosIncidence, dz.LOG_DEBUG)
            logWrite('Sum+ ' .. MAX_SUN_RADIATION + SUN_RADIATION_MARGING, dz.LOG_DEBUG)
            logWrite('Temp+ ' .. MAX_OUTSIDE_TEMP + OUTSIDE_TEMP_MARGING, dz.LOG_DEBUG)
            if currentBlindMode == 'Auto' then
                logWrite('Auto mode execution')
                
                if (lastBlindProgram == "Cold" and outsideTemp < MIN_OUTSIDE_TEMP + OUTSIDE_TEMP_MARGING) then
                    
                    newBlindProgram = "Cold"
                    logWrite('newBlindProgram stay Cold', dz.LOG_DEBUG)
                    wCold = true -- 01/06/2021
                    
                elseif (lastBlindProgram ~= "Cold" and outsideTemp < MIN_OUTSIDE_TEMP - OUTSIDE_TEMP_MARGING) then
                    
                    newBlindProgram = "Cold"
                    logWrite('newBlindProgram go Cold', dz.LOG_DEBUG)
                    wCold = true -- 01/06/2021
                    
                end
                if (lastBlindProgram == "Hot" and
                    sunRadiationValue > MAX_SUN_RADIATION - SUN_RADIATION_MARGING and
                    outsideTemp > MAX_OUTSIDE_TEMP - OUTSIDE_TEMP_MARGING) then -- 22/05/2022
                    
                    newBlindProgram = "Hot"
                    logWrite('newBlindProgram stay Hot', dz.LOG_DEBUG)
                    wHot = true -- 01/06/2021
                    
                elseif (lastBlindProgram ~= "Hot" and
                    sunRadiationValue > MAX_SUN_RADIATION + SUN_RADIATION_MARGING and
                    outsideTemp > MAX_OUTSIDE_TEMP + OUTSIDE_TEMP_MARGING) then -- 22/05/2022
                    
                    newBlindProgram = "Hot"
                    logWrite('newBlindProgram go Hot', dz.LOG_DEBUG)
                    wHot = true -- 01/06/2021
                    
                end
                logWrite('wHot ' .. tostring(wHot) .. ' wCold ' .. tostring(wCold), dz.LOG_DEBUG)
                if (wCold and wHot) or (not wCold and not wHot) then
                    
                    newBlindProgram = "Warm"
                    logWrite('newBlindProgram Warm', dz.LOG_DEBUG)
                    
                end
        
                local blindProgramChanged = false
                local logAction = '?' -- 06/07/2021
                if newBlindProgram ~= lastBlindProgram then
                    blindProgramChanged = true
                    logAction = 'changed to '
                else
                    logAction = 'remains on '
                end
                -- 11/08/2021
                logWrite(blindId .. ' ' .. BLIND_NAME .. ' - ' .. currentBlindMode .. ', ' .. logAction .. newBlindProgram .. ' - Outside Temp: ' .. _u.round(outsideTemp,1) .. '/[' .. MIN_OUTSIDE_TEMP .. ' - ' .. MAX_OUTSIDE_TEMP .. ']+/-' .. OUTSIDE_TEMP_MARGING .. ' - Sun criteria: ' .. sunRadiationValue .. '/' .. MAX_SUN_RADIATION .. '+/-' ..
SUN_RADIATION_MARGING, dz.LOG_FORCE)

                if lastSlatAngle == nil then -- on Auto mode, if initial position is unknown, the blind is open to get the zero -- move up 01/06/2021
                    logWrite('lastSlatAngle to init') 
                    lastSlatAngle = InitPosition(devBlind, SLAT_UP_ANGLE)
                end
                
                if newBlindProgram == "Hot" then
                    -- -- comment 01/06/2021
                    --if lastSlatAngle == nil then -- on Auto mode, if initial position is unknown, the blind is open to get the zero
                      --  logWrite('lastSlatAngle to init') 
                        --lastSlatAngle = InitPosition(devBlind)
                    --else
                        logWrite('Auto Program: ' .. newBlindProgram .. ' / Last Slat Angle '.. lastSlatAngle)            
                        local slatAngleTarget = CalculateSlatAngle(sunAzimuth, sunAltitude, BLIND_AZIMUTH, SLAT_DOWN_ANGLE, SLAT_UP_ANGLE, SOLAR_ALTITUDE_MIN, SLAT_LENGHT, SLAT_DISTANCE, BLIND_ANGLE, BLIND_TYPE, cosIncidence)
                        
                        if slatAngleTarget == nil then
                            
                            logWrite(blindId .. ' ' .. BLIND_NAME, dz.LOG_ERROR)
                            logWrite('sunAzimuth=' .. sunAzimuth, dz.LOG_ERROR)
                            logWrite('sunAltitude=' .. sunAltitude, dz.LOG_ERROR)
                            logWrite('BLIND_AZIMUTH=' .. BLIND_AZIMUTH, dz.LOG_ERROR)
                            logWrite('SLAT_DOWN_ANGLE=' .. SLAT_DOWN_ANGLE, dz.LOG_ERROR)
                            logWrite('SLAT_UP_ANGLE=' .. SLAT_UP_ANGLE, dz.LOG_ERROR)
                            logWrite('SOLAR_ALTITUDE_MIN=' .. SOLAR_ALTITUDE_MIN, dz.LOG_ERROR)
                            logWrite('SLAT_LENGHT"=' .. SLAT_LENGHT, dz.LOG_ERROR)
                            logWrite('SLAT_DISTANCE"=' .. SLAT_DISTANCE, dz.LOG_ERROR)
                            logWrite('BLIND_ANGLE"=' .. BLIND_ANGLE, dz.LOG_ERROR)
                            logWrite('BLIND_TYPE=' .. BLIND_TYPE, dz.LOG_ERROR)
                            logWrite('cosIncidence=' .. cosIncidence, dz.LOG_ERROR)
                            
                            slatAngleTarget = SLAT_UP_ANGLE
                        
                        end
                        
                        local slateMvt = 0
                        local lastSlatAngleDelta
                        slateMvt, lastSlatAngleDelta = CalculateSlatMvt(slatAngleTarget, lastSlatAngle, lastMaxSlatAngle, SLAT_UP_ANGLE, SLAT_DOWN_ANGLE, SLAT_MARGING, TTC_SLAT_SEC) -- mouvement to make to the slates in seconds
                        MoveSlat(slateMvt, devBlind)
                        if lastSlatAngleDelta == 999 then       -- it is the up position
                            lastSlatAngle = SLAT_UP_ANGLE
                            lastMaxSlatAngle = SLAT_UP_ANGLE -- 19/05/2022
                        elseif lastSlatAngleDelta == -999 then  -- it is the down position 
                            lastSlatAngle = SLAT_DOWN_ANGLE
                            lastMaxSlatAngle = SLAT_DOWN_ANGLE -- 19/05/2022
                        else
                            logWrite('lastSlatAngle ' .. lastSlatAngle)
                            lastSlatAngle = lastSlatAngle + lastSlatAngleDelta
                        end
                    --end
                elseif newBlindProgram == "Cold" then -- 08/08/2021 - bug! lastBlindProgram changed to newBlindProgram
                    --local solarAltitude = tonumber(devAltitude.state)
                    --logWrite("Solar Altitude=" .. tostring(solarAltitude))

                    --if dz.time.matchesRule('at 17:00-00:30') then -- !!! -> to put in parameters...  !!!!!!!!!!!!! 07/06/2022 to try!
                        --logWrite("timeConditionsClose OK")
                        if sunAltitude < -8 then
                            if devBlind.state ~= "Closed" then
                                devBlind.close()
                                devBlind.close().afterSec(5).silent() -- twice in case of loss of message
                                logWrite('Auto Program: ' .. newBlindProgram .. ' / ' .. devBlind.name .. " closing")
                            end
                            lastSlatAngle =  SLAT_DOWN_ANGLE
                            lastMaxSlatAngle = SLAT_DOWN_ANGLE -- 19/05/2022
                        else -- end
                    --elseif dz.time.matchesRule('at 6:00-12:00') then-- !!! -> to put in parameter 
                        --logWrite("timeConditionsOpen OK")
                        --if sunAltitude > -8 then
                            if devBlind.state ~= "Open" then
                                devBlind.open().silent()
                                devBlind.open().afterSec(5).silent()
                                logWrite('Auto Program: ' .. newBlindProgram .. ' / ' .. devBlind.name .. " opening")
                            end
                            lastSlatAngle =  SLAT_UP_ANGLE
                            lastMaxSlatAngle = SLAT_UP_ANGLE -- 19/05/2022
                        end
                    --end
                else -- Warm
                    --if dz.time.matchesRule('at 6:00-10:00') or blindProgramChanged then
                        --logWrite("Time to check if it is open")
                        --local solarAltitude = tonumber(devAltitude.state)
                        --logWrite("Solar Altitude=" .. tostring(solarAltitude))
                        if sunAltitude > -8 then
                            if devBlind.state ~= "Open" then
                                devBlind.open()
                                logWrite('Auto Program: ' .. newBlindProgram .. ' / ' .. devBlind.name .. " opening")
                            end
                            lastSlatAngle =  SLAT_UP_ANGLE
                            lastMaxSlatAngle = SLAT_UP_ANGLE -- 19/05/2022
                        end
                    --end
                end
    
            else   
                newBlindProgram = ''
            end
                
        
            -- < Update global variables if changed
            local renameFlag = false
            if dz.data.blindMode[blindId] ~= currentBlindMode then
                renameFlag = true
                logWrite('currentBlindMode changed to ' .. currentBlindMode, dz.LOG_FORCE)
                dz.data.blindMode[blindId] = currentBlindMode
            else
                logWrite('currentBlindMode NOT changed: ' .. currentBlindMode)
            end
            
            if dz.data.blindProgram[blindId] ~= newBlindProgram then
                renameFlag = true
                if newBlindProgram == '' then -- 07/02/2021 clarify log if newBlindProgram is empty
                    logWrite('newBlindProgram changed to NONE', dz.LOG_FORCE)                
                else
                    logWrite('newBlindProgram changed to ' .. newBlindProgram, dz.LOG_FORCE)
                 end
                dz.data.blindProgram[blindId] = newBlindProgram
            else
                logWrite('newBlindProgram NOT changed: ' .. newBlindProgram)
            end

            if (dz.data.slatAngle[blindId] ~= lastSlatAngle) then -- 16/08/2021
                renameFlag = true
                if lastSlatAngle == nil then
                    logWrite('SlatAngle changed to nul')
                else
                    if lastSlatAngle == SLAT_DOWN_ANGLE then
                        logWrite(devBlind.name ..  ' closes ', dz.LOG_FORCE)
                    elseif lastSlatAngle == SLAT_UP_ANGLE then
                        logWrite(devBlind.name ..  ' opens ', dz.LOG_FORCE)
                    else
                        local newSlatAngle = _u.round(lastSlatAngle,0)
                        logWrite('Slates of ' .. devBlind.name .. ' move to ' .. newSlatAngle .. '° - ' .. newBlindProgram, dz.LOG_FORCE)
                    end
                end
                dz.data.slatAngle[blindId] = lastSlatAngle
                dz.data.lastMaxSlatAngle[blindId] = lastMaxSlatAngle -- 19/05/2022
            else    
                if lastSlatAngle == nil then
                    logWrite('SlatAngle NOT changed: nul')
                else
                    if lastSlatAngle == SLAT_DOWN_ANGLE then
                        logWrite(devBlind.name ..  ' remains closed ')
                    elseif lastSlatAngle == SLAT_UP_ANGLE then
                        logWrite(devBlind.name ..  ' remains open ')
                    else
                        logWrite('Slates of ' .. devBlind.name .. ' remains on ' .. lastSlatAngle .. ' °')
                    end
                end
            end
        
            if renameFlag then -- 16/08/2021
                local blindRename = BLIND_NAME
                if lastSlatAngle == nil then
                    blindRename = blindRename .. ' (' .. devBlind.state .. ') - '  .. currentBlindMode
                else
                    if lastSlatAngle == SLAT_DOWN_ANGLE then
                        blindRename = blindRename .. ' (Closed) - ' .. currentBlindMode
                    elseif lastSlatAngle == SLAT_UP_ANGLE then
                        blindRename = blindRename .. ' (Open) - ' .. currentBlindMode
                    else
                        local newSlatAngle = _u.round(lastSlatAngle,0)
                        blindRename = blindRename .. ' (' .. newSlatAngle .. '°) - ' .. currentBlindMode
                    end
                end
                blindRename = blindRename .. ' ' .. newBlindProgram
                devDummyBlind.rename(blindRename)
                devDummyBlind.switchSelector(devDummyBlind.level).silent()
            end
        end

    end
}


script updated on 04-June 2022
script updated on 12/06/2022 /!\ Breaking change BLIND_AZIMUTH is now the Azimuth of the normal of the area
script updated on 20/06/2022

You can declare several blinds even if they are not on the same frontage.
In my case, it is set up for 2 blinds:
- a sunshade on a sloping roof
lames.png
lames.png (169.15 KiB) Viewed 9136 times
- a vertical blind
A selector to operate the blind and especially to switch to Auto mode
Selector.png
Selector.png (24.22 KiB) Viewed 9136 times
Brise-soleil_26.png
Brise-soleil_26.png (12.71 KiB) Viewed 6996 times
Last edited by hestia on Wednesday 21 September 2022 21:44, edited 7 times in total.
EddyG
Posts: 1042
Joined: Monday 02 November 2015 5:54
Target OS: -
Domoticz version:

Re: blinds move on the sun

Post by EddyG »

Nice!
I have a totally different question: how do you set those icons on the buttons.
User avatar
waltervl
Posts: 5727
Joined: Monday 28 January 2019 18:48
Target OS: Linux
Domoticz version: 2024.7
Location: NL
Contact:

Re: blinds move on the sun

Post by waltervl »

Nice project. How did you get these up/down/stop symbols in your selector switch? By using special characters?
Domoticz running on Udoo X86 (on Ubuntu)
Devices/plugins: ZigbeeforDomoticz (with Xiaomi, Ikea, Tuya devices), Nefit Easy, Midea Airco, Omnik Solar, Goodwe Solar
besix
Posts: 99
Joined: Friday 25 January 2019 11:33
Target OS: Linux
Domoticz version: beta
Location: Poland
Contact:

Re: blinds move on the sun

Post by besix »

These icons are explained
10: &#9650 => UP
20: &#9724 => STOP
30: &#9660 => DOWN
40: A => Auto

Selector level name &#9650 gives UP
User avatar
waltervl
Posts: 5727
Joined: Monday 28 January 2019 18:48
Target OS: Linux
Domoticz version: 2024.7
Location: NL
Contact:

Re: blinds move on the sun

Post by waltervl »

besix wrote: Wednesday 14 July 2021 13:25 These icons are explained
10: &#9650 => UP
20: &#9724 => STOP
30: &#9660 => DOWN
40: A => Auto

Selector level name &#9650 gives UP
Good find!
It seems a lot more possibilities are available: https://www.w3schools.com/charsets/ref_ ... metric.asp
Domoticz running on Udoo X86 (on Ubuntu)
Devices/plugins: ZigbeeforDomoticz (with Xiaomi, Ikea, Tuya devices), Nefit Easy, Midea Airco, Omnik Solar, Goodwe Solar
besix
Posts: 99
Joined: Friday 25 January 2019 11:33
Target OS: Linux
Domoticz version: beta
Location: Poland
Contact:

Re: blinds move on the sun

Post by besix »

There are so many possibilities : https://unicode-table.com/en/html-entities/
hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

Re: blinds move on the sun

Post by hestia »

besix wrote: Thursday 15 July 2021 20:45 There are so many possibilities : https://unicode-table.com/en/html-entities/
It seems that now it is the main subject of the topic ;-)
rrozema
Posts: 470
Joined: Thursday 26 October 2017 13:37
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Location: Delft
Contact:

Re: blinds move on the sun

Post by rrozema »

Very nice script Hestia. I don't need it yet, but I've bookmarked it for the future. Thank you!
hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

Re: blinds move on the sun

Post by hestia »

Thanks for your comment :)
In the end of the summer I found an issue regarding the sun radiation.
The result of the calculation of the solar radiation (lux or W/m2) from the very good script of jmleglise is considered on the floor (horizontal projection of the sun radiation). It seems that it's also the same for weather stations (measured on an horizontal surface).
So it is not good at the end of the afternoon because the impact of the sun is reduced!
I've changed the script to take it into account (and also the solar script).
I hadn't published it ; it's done now!
Next step my Velux shutters...
Mike70
Posts: 21
Joined: Thursday 22 October 2015 19:46
Target OS: Raspberry Pi / ODroid
Domoticz version: bèta
Location: Netherlands
Contact:

Re: blinds move on the sun

Post by Mike70 »

I got blinds and tried to use the script but I ran int a few problems.
I also use the script to calculate the LUX and sun position.
I made a selector with the pictograms you mentioned.
But if I delete the off (Niveau 0), the first pictogram (Niveau 10) would become Niveau 0.
Can you provide a screenshot of the selector settings?
The script also contains the next lines:
local BLINDS =
{ [1203] = -- Dummy device with Automatics command (to create) (id or 'name')
['BLIND_ID'] = 953, -- Device of the BLIND to close or open (id or 'name')

I presume 1203 is the idx of the selector?
I also think that ['BLIND_ID'] ='953' is the idx of the blinds you want to control?
am I right?
hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

Re: blinds move on the sun

Post by hestia »

here is a screen shot of one blind
Screenshot 2022-02-24 184629.png
Screenshot 2022-02-24 184629.png (74.64 KiB) Viewed 8623 times
I presume 1203 is the idx of the selector?
=> YES
I also think that ['BLIND_ID'] ='953' is the idx of the blinds you want to control?
=> YES

I've just remembered that I've made an addition in the script that calculate the LUX and sun position to get SUN_CRITERIA1 as the direct radiation and SUN_CRITERIA2 as the scattered (or diffuse) radiation to be able to calculate the total radiation on the surface of the blind with the angle of this surface and the sun

Code: Select all

-- SUN_CRITERIA: 1 or 2 devices to determine if the blind must be closed (according to the sun position) because it is too sunny
-- It could be temperature, solar radiation in lux, or solar radiation in W/m2
-- If solar radiation, there could be 1 or 2 devices
-- If 1 device, it is a total radiation value (usually on the ground)
-- If 2 device, the SUN_CRITERIA1 is the direct radiation and SUN_CRITERIA2 is the scattered (or diffuse) radiation
-- and the total radiation is calculated on the surface of the blind with the angle of this surface and the sun
But you could start with 1 device, the total radiation value (usually on the ground). It enough for winter.
I'll give the script updated in a while if you need it
Mike70
Posts: 21
Joined: Thursday 22 October 2015 19:46
Target OS: Raspberry Pi / ODroid
Domoticz version: bèta
Location: Netherlands
Contact:

Re: blinds move on the sun

Post by Mike70 »

Great. I've got it working so far.

Just got errors when I pres a button on the selectors.
Maybe you can help me with that:
Error opening url: http://0.0.0.0:8080/json.htm?type=comma ... +-+INITIAL+
Error opening url: http://0.0.0.0:8080/json.htm?type=comma ... +Auto+Cold
The IP addres should be: http://192.168.1.130:8080.... but I can not find a line to put the IP addres in your script.
Or should I use the Selector actions: ?
I am not familiair with DzVents and selector switches.

At the moment I use only the SUN_CRITERIA1 and it uses the idx of the LUX.
So I think it is enough for me at the moment.
But if you have an update of the script, I'll be happy to use it.
hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

Re: blinds move on the sun

Post by hestia »

you could put more log with

Code: Select all

domoticz.LOG_DEBUG,

local LOG_LEVEL = dz.LOG_DEBUG
That's an error about the rename. I don't know why, a dz / dzvents error
Perhaps with more log you could have more info

Let it run for some time to check if the blinds open and close as you want
rrozema
Posts: 470
Joined: Thursday 26 October 2017 13:37
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Location: Delft
Contact:

Re: blinds move on the sun

Post by rrozema »

Mike70 wrote: Thursday 24 February 2022 18:02 I also think that ['BLIND_ID'] ='953' is the idx of the blinds you want to control?
am I right?
Actually, NO, you're wrong.

There's a big difference between

Code: Select all

['BLIND_ID']   ='953'
and

Code: Select all

['BLIND_ID']   =953
and this can very well be the source of your errors. If you enclose a value in single (') or double quotes ("), you make that value a string. Without the quotes the value will be a number. Domoticz and dzvents use that difference to determine whether you want to specify the device by name (= string enclosed in single or double quotes) or by idx (= number without the quotes). So ['BLIND_ID'] ='953' tells dzvents to look for a device with the name of the blinds you want to control, not the idx, if you want to specify the device by it's idx, you should not enclose that value with quotes or domoticz won't be able to find a device with the name '953'. I hope this helps you find the cause of your problems.
Mike70
Posts: 21
Joined: Thursday 22 October 2015 19:46
Target OS: Raspberry Pi / ODroid
Domoticz version: bèta
Location: Netherlands
Contact:

Re: blinds move on the sun

Post by Mike70 »

@rrozema.
You are right. I Made a mistake to use quotes in the mail.
In the script they are right.
I have to look further what I am doing wrong.
hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

Re: blinds move on the sun

Post by hestia »

@rrozema,
thanks for your good look!
@Mike70, is the script working now?
You could put a name w quotes or an idx w/o, only tested w idx, but should works due to 'dz.devices()'

Code: Select all

local devDummyBlind = dz.devices(blindId)               -- Switch dummy device with commands 
local devBlind = dz.devices(BLINDS[blindId].BLIND_ID)   -- Device of the BLIND to close or open
EDIT 26/02/2022: the name doesn't work for the dummy because it's going to be rename in the script... should work for all other devices
Last edited by hestia on Saturday 26 February 2022 18:20, edited 1 time in total.
Mike70
Posts: 21
Joined: Thursday 22 October 2015 19:46
Target OS: Raspberry Pi / ODroid
Domoticz version: bèta
Location: Netherlands
Contact:

Re: blinds move on the sun

Post by Mike70 »

It is getting better.
I found a reply from waltervl in an other discussion.

Also make sure that in the Security section in the settings (Setup > Settings > System > Local Networks (no username/password) you allow 127.0.0.1 (and / or ::1 when using IPv6 ) to not need a password. dzVents does use this port to get the location settings and to send certain commands to Domoticz.

I did not used the value 127.0.0.1 but only 192.168.1.*
Now the error is gone and the selector is renamed into the right value.
:D
Tomorrow I have to change the polarity of the blinds and see if it works.
But for now. I think I got it working.
Thanks again for the useful script.!
hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

Re: blinds move on the sun

Post by hestia »

I'm happy that the script is working for you!
As you wanted the new solar script, I've post it Solar Data script : Azimuth, Altitude, Lux
You'll have to change 2 idx:
SUN_CRITERIA1 => the direct radiation,
SUN_CRITERIA2 => the scattered radiation
both "weighted" = with cloud impact, given by the cloud coverage
rrozema
Posts: 470
Joined: Thursday 26 October 2017 13:37
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Location: Delft
Contact:

Re: blinds move on the sun

Post by rrozema »

Hi Hestia, I was trying to apply your script for my roller blinds, but I think this is not what you intended it for, so I think I'll add a new blind type "R" for roller blinds that only move up and down, with no slats at all.

Also I see that you're applying timing to run the slats to a specific angle. Does your slats device not provide a percentage slider for setting the angle of the slats? Or do you have a specific reason not to use the device's percentage setting?
hestia
Posts: 361
Joined: Monday 25 December 2017 23:06
Target OS: Raspberry Pi / ODroid
Domoticz version: 2022.1
Location: Paris
Contact:

Re: blinds move on the sun

Post by hestia »

Hi rrozema
I was trying to apply your script for my roller blinds [...] that only move up and down, with no slats at all.
I have this one that I think is like yours (up and down only)
roller.png
roller.png (354.7 KiB) Viewed 8027 times
It's the 1st one in the script => BLIND_TYPE'] = 'V'

Code: Select all

local BLINDS = 
    {   [1203] =                            -- Dummy device with Automatics command (to create) (id or 'name')
            {
            ['BLIND_TYPE'] = 'V',           -- "B" Brise Soleil or "V" Vertical
            ['BLIND_NAME'] = 'Store',-- Name of the dummy device
            ['BLIND_ID'] = 953,             -- Device of the BLIND to close or open (id or 'name') 
            ['BLIND_AZIMUTH'] = 199,        -- Angle from North to where the sun is // to the blind (like the Azimuth)
            ['SOLAR_ALTITUDE_MIN'] = 10,    -- Solar Altitude where the sun is hidden by a house, a tree, if nothing 0    
            ['MAX_SUN_CRITERIA'] = ALL_MAX_SUN_CRITERIA,   -- If criteria is more than this max critéria, the blind is closed during the day and according to the sun position (temperature, UV...) (HOT)
            ['MIN_OUTSIDE_TEMP']  = ALL_MIN_OUTSIDE_TEMP,     -- If outside temperature in less than this min temp, the blind is closed night (COLD) 
            ['SLAT_DOWN_ANGLE']  = -90,     -- Angle from the horizontal when the slat is down (<0), -90 for vertical blind
            ['SLAT_UP_ANGLE'] = 90,         -- Angle from the horizontal when the slat is up, 90 for vertical blind
            ['SLAT_LENGHT']  = '',          -- Length of the slate, any unit (cm...), nil for vertical blind
            ['SLAT_DISTANCE']  = '',        -- Distance between slates, any unit (cm...), nil for vertical blind
            ['BLIND_ANGLE']  = 90,          -- Angle of the blind from the horizontal (>0)
            ['TTC_SLAT_SEC'] = 1            -- Time To Close the blind in seconds, to be measured, 1 for vertical blind 
            },
But 'R' as Roller is a better code name, if my 'V' script is ok for you, I could change, to make it easier to understand - also I had thought that rollers are only vertical ;-) but that's not right because I have roller (shutter) on my Velux roof!
Also I see that you're applying timing to run the slats to a specific angle. Does your slats device not provide a percentage slider for setting the angle of the slats? Or do you have a specific reason not to use the device's percentage setting?
I have RFY / RFY roller and brise-soleil
rfy.png
rfy.png (24.43 KiB) Viewed 8027 times
I wasn't aware that I could give directly an angle and my devices (slats and roller) do not provide a percentage slider, perhaps I didn't configure them correctly?
What do you have on you side?

I'm working on a new version to improve things regarding what I was experiencing with summer arriving...
19/05/2022 - adjust the rounded of the movement to avoid a line of sun when the brise-soleil are closing
22/05/2022 - separate sun radiation and max outside temperature to combine them
next? - add HOT+ program: shut the blinds if very hot and the sun strikes the blind !!!!!!!!!!!!!!!!!!
I could publish the script if interested
Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests