I made my car charge on excess solar energy only

The goal of this project is to start charging the car when my solar panels generate so much energy, that I can charge the car with my own electric energy. This way, I do not have to pay the electricity company. Consuming your own generated energy is much more financially efficient than selling energy when the sun shines and buying it back when there are clouds or when it is dark. I do not have a battery system (since I automate consumption like described here).

My hybrid car is charging at my home via a standard 16A wall socket. This outlet is wired in the house via an extra 30A circuit breaker / relay that is controlled by Tasmota from Athom. I have set the charger to charge with 6A (1400 Watt).

The electricity entering my house is measured by the electricity supplier measurement system, which has a “P1” digital data output, which I capture with a Youless module. It has a simple API over HTTP to provide all the measurements every second to the domotica system.

The domotica system I use in the house is Domoticz, and it has registered both the energy measurements device and the outlet relay.

Then I use the Lua language in Domoticz to control the relay according the measurements: If there is an excess of 1400W of energy, then I switch the car charger on, and if I start buying energy from the electricity company again, I switch the charger off.

All this has to happen with some delays and margins, since the consumption and production of energy can vary wildly.

So, I drew an UML statechart to clarify the Lua code before I wrote it:

Statechart

In the code, I really used the statechart to the letter:

  • The transitions are implemented as separate functions.
  • The guards are implemented as separate functions, each returning a Boolean.
  • On every “tick” of the clock, we receive new electricity measurements, and determine if there is any transition to be taken in the function determineTransition.
  • This function determineTransition checks in which of the 3 states we are, and checks the appropriate guards - just like in the diagram. Its return parameter is the transition function!
  • If a transition is found, execute the transition:
local t = determineTransition();
if (t) then t() end

This is the full code:

-- This script uses a Tasmota 30A Breaker to switch the car charger 
-- on and off, depending on the availability of excess power 
-- from the solar panels. 
 
-- So, if there is no sun, we do not have enough electricity production, and we 
-- do not charge the car.
-- If the sun is shining, and we do not consume all the produced electricity 
-- ourselves, then, instead of delivering the excess to the grid, 
-- we charge the car.

-- The car charger is powered via a 30A circuit breaker flashed with Tasmota, 
-- registered in Domoticz as device 422, named "Car Charger" in Domoticz.

-- Then there is a way to demand to charge, 
-- i.e. you can switch the automaton on or off: 
-- domoticz.devices(479) is a dummy device with a car icon 
-- that can be toggled manually. 
-- If this is off, then the algorithm here 
-- switches the car charger off unconditionally.

-- The car charger used here consumes (minimum) 1400W, which is 
-- 6A at 235V, the minimum setting on the charger cable.
-- A global data setting named CarChargerConsumedPower contains this value.
-- The car charger can be set to another power level, 
-- and if this is done, we need to adjust 
-- the CarChargerConsumedPower value, too.
-- Maybe this can be turned into an adjustable value later.



return {
	on = {
		devices = {
			'Youless Power'  -- 393
		}
	},
	logging = {
		level = domoticz.LOG_ERROR,
		marker = 'SolarCarCharger',
	},
	data = {
	    -- The ccenablecounter counts down until 
	    -- the excess power is available for long enough:
	    ccenablecounter = { initial = 10 }; -- domoticz.helpers.CarChargerEnableDelay
        CarChargerStatus = {initial = 1}; -- the status of the car charger: OFF=1, EXCESS_DETECTED=2, CHARGING=3
	},
	execute = function(domoticz, device)

		local actualPowerUsage -- The house is consuming this amount of power (measured in Watt), negative for production
		actualPowerUsage = (device.usage - device.usageDelivered);
		domoticz.log('House Power usage ' .. actualPowerUsage .. ' W.', domoticz.LOG_INFO);
		
		local carChargeDemand -- the device for the user to demand heat
		carChargeDemand = domoticz.devices(479);
		domoticz.log('Solar car charger demand switch is ' .. carChargeDemand.state, domoticz.LOG_INFO);
		--domoticz.log('Solar car charger demand switch is ' .. (carChargeDemand.active and "T" or "F"), domoticz.LOG_INFO);
		
		
		local carChargerSwitchDevice -- the device that switches the car charger on and off
		carChargerSwitchDevice = domoticz.devices(422);

		-- the status of the car charger: OFF=1, EXCESS_DETECTED=2, CHARGING=3

		-- ==================================
		-- Transitions
		-- ==================================
		local function transition_charging_off()
		    -- the car charger shall be off
		    carChargerSwitchDevice.switchOff().checkFirst();
		    domoticz.data.CarChargerStatus = 1 --OFF;
		end
		
		local function transition_excessdetected_off()
		    -- the car charger shall be off
		    carChargerSwitchDevice.switchOff().checkFirst();
		    domoticz.data.CarChargerStatus = 1 --OFF;
		end
		
		local function transition_off_excessdetected()
		    domoticz.data.ccenablecounter = domoticz.helpers.CarChargerEnableDelay;
		    domoticz.data.CarChargerStatus = 2 --EXCESS_DETECTED;
		    -- domoticz.log('Transition: 1 executed.', domoticz.LOG_INFO);
		end

		local function transition_excessdetected_excessdetected()
		    domoticz.data.ccenablecounter = domoticz.data.ccenablecounter - 1;
		    -- domoticz.data.CarChargerStatus = 2 --EXCESS_DETECTED;
		end

		local function transition_excessdetected_charging()
			-- the car charger shall be on
		    carChargerSwitchDevice.setState('On');
		    domoticz.data.CarChargerStatus = 3 --CHARGING;
		end

		-- ==================================
		-- Guards
		-- ==================================
		local function guard_demand()
			return carChargeDemand.active;
		end
		
		local function guard_nodemand()
			return carChargeDemand.inActive;
		end
		
		local function guard_excessdetected()
			-- the house is producing more power than the car charger would consume
			return ( (domoticz.helpers.CarChargerConsumedPower / 1) < (- actualPowerUsage) );
		end

		local function guard_nopoweravailable()
			-- the sun is gone
   		    -- there is no available excess power
			return ( 300 < (actualPowerUsage) );
		end

		local function guard_delaycountnotreached()
			return (domoticz.data.ccenablecounter > 0);
		end
	
		local function guard_delaycountreached()
			return (domoticz.data.ccenablecounter < 1);
		end


		-- ==================================
		-- Determine Transition
		-- ==================================
		local function determineTransition()
			if (domoticz.data.CarChargerStatus == 1 )--OFF
			then
			    -- domoticz.log('Current status 1: ' .. domoticz.data.CarChargerStatus, domoticz.LOG_INFO);
				if (guard_demand() and guard_excessdetected()) 
				then 
					domoticz.log('Transition: 1 ', domoticz.LOG_INFO);
					return transition_off_excessdetected
				-- else
				--    domoticz.log('NO transition: 1, demand = ' 
				--        ..  (guard_demand()and "T" or "F") 
				--        .. ", excessdetected = " 
				--        ..  (guard_excessdetected()and "T" or "F"), 
				--            domoticz.LOG_INFO);
				end
			elseif ((domoticz.data.CarChargerStatus) == 2) --EXCESS_DETECTED
			then
			    -- domoticz.log('Current status 2: ' .. domoticz.data.CarChargerStatus, domoticz.LOG_INFO);
				if (guard_nodemand() or not guard_excessdetected()) 
				then 
					domoticz.log('Transition: 2 ', domoticz.LOG_INFO);
					-- the car charger shall be off
					return transition_excessdetected_off
				end
				if (guard_delaycountnotreached() and guard_excessdetected() and guard_demand()) 
				then 
					domoticz.log('Transition: 3 ', domoticz.LOG_INFO);
					return transition_excessdetected_excessdetected
				end
				if (guard_delaycountreached() and guard_excessdetected() and guard_demand()) 
				then 
					domoticz.log('Transition: 4 ', domoticz.LOG_INFO);
					-- the car charger shall be on
					return transition_excessdetected_charging
				end
			elseif (domoticz.data.CarChargerStatus == 3) --CHARGING
			then
			    -- domoticz.log('Current status 3: ' .. domoticz.data.CarChargerStatus, domoticz.LOG_INFO);
				if (guard_nodemand() or guard_nopoweravailable()) 
				then 
					domoticz.log('Transition: 5 ', domoticz.LOG_INFO);
					-- the car charger shall be off
					return transition_charging_off
				end
			else
				domoticz.log('Case: 99 : serious trouble', domoticz.LOG_ERROR);
			end
		end

		local t = determineTransition();
		if (t) then t() end

        domoticz.log('Current status: ' .. domoticz.data.CarChargerStatus, domoticz.LOG_INFO);
        if ((domoticz.data.CarChargerStatus) == 2) then 
            domoticz.log('Current enable counter: ' .. domoticz.data.ccenablecounter, domoticz.LOG_INFO);
        end
        domoticz.log('The Car Charger is now ' .. carChargerSwitchDevice.state .. '.', 
			domoticz.LOG_INFO);
    end
}


To make this work in Domoticz, the global_data script contains the following:

return {
	helpers = {
        CarChargerConsumedPower = 1400, -- (minimum) consumption of the car charger
        CarChargerEnableDelay = 20, -- the delay before the consumer is enabled, measured in 5 second periods
	}
}