diff --git a/src/solarflow/dtus.py b/src/solarflow/dtus.py index 248a740..478437b 100644 --- a/src/solarflow/dtus.py +++ b/src/solarflow/dtus.py @@ -48,7 +48,7 @@ def __str__(self): return ' '.join(f'{yellow}INV: \ AC:{self.getCurrentACPower():>3.1f}W, AC_Prediction: {self.getPredictedACPower():>3.1f}W, \ DC:{self.getCurrentDCPower():>3.1f}W, DC_prediction: {self.getPredictedDCPower():>3.1f}W ({chPower}), \ - L:{self.limitAbsolute:>3}W [{self.maxPower:>3}W]{reset}'.split()) + L:{self.limitAbsolute:>3.0f}W ({self.getChannelLimit():.1f}W/channel) [{self.maxPower:>3.0f}W]{reset}'.split()) def subscribe(self, topics): topics.append(f'solarflow-hub/+/control/dryRun') @@ -191,6 +191,9 @@ def setDryRun(self,value): def isWithin(self,a,b,range:int): return b-range < a < b+range + def getChannelLimit(self) ->int: + return self.getLimit()/(len(self.channelsDCPower)-1) + def setLimit(self, limit:int): # failsafe, never set the inverter limit to 0, keep a minimum # see: https://github.com/lumapu/ahoy/issues/1079 @@ -210,22 +213,25 @@ def setLimit(self, limit:int): # it could be that maxPower has not yet been detected resulting in a zero limit inv_limit = 10 if inv_limit < 10 else int(inv_limit) + withinRange = 6 # failsafe: ensure that the inverter's AC output doesn't exceed acceptable legal limits # note this could mean that the inverter limit is still higher but it ensures that not too much power is generated if self.getCurrentACPower() > self.acLimit and inv_limit > self.acLimit: # decrease inverter limit slowly - inv_limit = self.limitAbsolute - 4 + inv_limit = self.limitAbsolute - 8 + withinRange = 0 log.info(f'Current inverter AC output ({self.getCurrentACPower()}) is higher than configured output limit ({self.acLimit}), reducing limit to {inv_limit}') # failsafe: if the current AC output is close to the AC limit do not increase the invert limit too much if self.getCurrentACPower() < self.acLimit and self.isWithin(self.getCurrentACPower(), self.acLimit, 6): # only increase inverter limit a little bit inv_limit = self.limitAbsolute + 2 + withinRange = 0 log.info(f'Current inverter AC output ({self.getCurrentACPower()}) is close to the configured AC output limit ({self.acLimit}), slow limit increase to {inv_limit}') #if self.limitAbsolute != inv_limit and self.reachable: - if not self.isWithin(inv_limit,self.limitAbsolute,6) and self.reachable: + if not self.isWithin(inv_limit,self.limitAbsolute,withinRange) and self.reachable: (not self.dryrun) and self.client.publish(self.limit_nonpersistent_absolute,f'{inv_limit}{self.limit_unit}') #log.info(f'Setting inverter output limit to {inv_limit} W ({limit} x 1 / ({len(self.sf_inverter_channels)}/{len(self.channelsDCPower)-1})') log.info(f'{"[DRYRUN] " if self.dryrun else ""}Setting inverter output limit to {inv_limit}W (1 min moving average of {limit}W x {len(self.channelsDCPower)-1})') diff --git a/src/solarflow/solarflow-control.py b/src/solarflow/solarflow-control.py index a5a237f..bd7fd12 100644 --- a/src/solarflow/solarflow-control.py +++ b/src/solarflow/solarflow-control.py @@ -144,6 +144,8 @@ def on_connect(client, userdata, flags, rc): hub = client._userdata['hub'] hub.subscribe() hub.setBuzzer(False) + if hub.control_bypass: + hub.setBypass(False) inv = client._userdata['dtu'] inv.subscribe() smt = client._userdata['smartmeter'] @@ -187,7 +189,7 @@ def getDirectPanelLimit(inv, hub, smt) -> int: direct_panel_power = inv.getDirectACPower() + inv.getHubACPower() if hub.getBypass() else 0 if direct_panel_power < MAX_INVERTER_LIMIT: dc_values = inv.getDirectDCPowerValues() + inv.getHubDCPowerValues() if hub.getBypass() else inv.getDirectDCPowerValues() - return math.ceil(max(dc_values) * (inv.getEfficiency()/100)) if smt.getPower()- smt.zero_offset < 0 else limitedRise(max(dc_values) * (inv.getEfficiency()/100)) + return math.ceil(max(dc_values) * (inv.getEfficiency()/100)) if smt.getPower() - smt.zero_offset < 0 else limitedRise(max(dc_values) * (inv.getEfficiency()/100)) else: return int(MAX_INVERTER_LIMIT*(inv.getNrHubChannels()/inv.getNrTotalChannels())) @@ -207,12 +209,12 @@ def getSFPowerLimit(hub, demand) -> int: if hub.bypass: path += "0." # leave bypass after sunset/offset - if (now < (sunrise + sunrise_off) or now > sunset - sunset_off): + if (now < (sunrise + sunrise_off) or now > sunset - sunset_off) and hub.control_bypass: hub.setBypass(False) path += "1." else: path += "2." - limit = demand + limit = hub.getInverseMaxPower() if not hub.bypass: if hub_solarpower - demand > MIN_CHARGE_POWER: @@ -241,6 +243,9 @@ def getSFPowerLimit(hub, demand) -> int: if now > sunrise and now < sunrise + td: hub.setSunriseSoC(hub_electricLevel) log.info(f'Good morning! We have consumed {hub.getNightConsumption()}% of the battery tonight!') + # sometimes bypass resets to default (auto) + if hub.control_bypass: + hub.setBypass(False) log.info(f'Based on time, solarpower ({hub_solarpower:4.1f}W) minimum charge power ({MIN_CHARGE_POWER}W) and bypass state ({hub.bypass}), hub could contribute {limit:4.1f}W - Decision path: {path}') return int(limit) @@ -287,26 +292,37 @@ def limitHomeInput(client: mqtt_client): if demand < direct_panel_power: # we can conver demand with direct panel power, just use all of it log.info(f'Direct connected panels ({direct_panel_power:.1f}W) can cover demand ({demand:.1f}W)') + #direct_limit = getDirectPanelLimit(inv,hub,smt) + # keep inverter limit where it is, no need to change direct_limit = getDirectPanelLimit(inv,hub,smt) hub_limit = hub.setOutputLimit(0) else: - # we need contribution from hub, if possible + # we need contribution from hub, if possible and/or try to get more from direct panels log.info(f'Direct connected panels ({direct_panel_power:.1f}W) can\'t cover demand ({demand:.1f}W), trying to get {hub_contribution_ask:.1f}W from hub.') if hub_contribution_ask > 5: - # check what hub is currently willing to contribute - sf_contribution = getSFPowerLimit(hub,hub_contribution_ask) - - # if the hub's contribution (per channel) is larger than what the direct panels max is delivering (night, low light) - # then we can open the hub to max limit and use the inverter to limit it's output (more precise) - if sf_contribution/inv.getNrHubChannels() >= max(inv.getDirectDCPowerValues()) * (inv.getEfficiency()/100): - log.info(f'Hub should contribute more ({sf_contribution:.1f}W) than what we currently get from panels ({direct_panel_power:.1f}W), we will use the inverter for fast/precise limiting!') - hub_limit = hub.setOutputLimit(hub.getInverseMaxPower()) - direct_limit = sf_contribution/inv.getNrHubChannels() - else: - hub_limit = hub.setOutputLimit(sf_contribution) - log.info(f'Solarflow is willing to contribute {min(hub_limit,hub_contribution_ask):.1f}W of the requested {hub_contribution_ask:.1f}!') + # is there potentially more to get from direct panels? + # if the direct channel power is below what is theoretically possible, it is worth trying to increase the limit + + # if the max of direct channel power is close to the channel limit we should increase the limit first to get pot more from direct panels + if inv.isWithin(max(inv.getDirectDCPowerValues()),inv.getChannelLimit(),10): + log.info(f'The current max direct channel power {max(inv.getDirectDCPowerValues()):.1f}W is close to the current channel limit {inv.getChannelLimit():.1f}, trying to get more from direct panels.') + hub_limit = hub.getLimit() direct_limit = getDirectPanelLimit(inv,hub,smt) - log.info(f'Direct connected panel limit is {direct_limit}W.') + else: + # check what hub is currently willing to contribute + sf_contribution = getSFPowerLimit(hub,hub_contribution_ask) + + # if the hub's contribution (per channel) is larger than what the direct panels max is delivering (night, low light) + # then we can open the hub to max limit and use the inverter to limit it's output (more precise) + if sf_contribution/inv.getNrHubChannels() >= max(inv.getDirectDCPowerValues()) * (inv.getEfficiency()/100): + log.info(f'Hub should contribute more ({sf_contribution:.1f}W) than what we currently get from panels ({direct_panel_power:.1f}W), we will use the inverter for fast/precise limiting!') + hub_limit = hub.setOutputLimit(hub.getInverseMaxPower()) + direct_limit = sf_contribution/inv.getNrHubChannels() + else: + hub_limit = hub.setOutputLimit(sf_contribution) + log.info(f'Solarflow is willing to contribute {min(hub_limit,hub_contribution_ask):.1f}W of the requested {hub_contribution_ask:.1f}!') + direct_limit = getDirectPanelLimit(inv,hub,smt) + log.info(f'Direct connected panel limit is {direct_limit}W.') # likely no sun, not producing, eveything comes from hub else: diff --git a/src/solarflow/solarflow.py b/src/solarflow/solarflow.py index 4062280..f458deb 100644 --- a/src/solarflow/solarflow.py +++ b/src/solarflow/solarflow.py @@ -139,15 +139,12 @@ def updElectricLevel(self, value:int): self.setBypass(True) self.lastFullTS = datetime.now() - if self.control_bypass: - log.info(f'Bypass control, turning on bypass!') - self.setBypass(True) self.client.publish(f'solarflow-hub/{self.deviceId}/control/lastFullTimestamp',int(datetime.timestamp(self.lastFullTS)),retain=True) self.client.publish(f'solarflow-hub/{self.deviceId}/control/batteryTarget',"discharging",retain=True) if value == 0: if self.batteryTarget == "discharging": log.info(f'Battery is empty: {self.electricLevel}') - + self.lastEmptyTS = datetime.now() self.client.publish(f'solarflow-hub/{self.deviceId}/control/lastEmptyTimestamp',int(datetime.timestamp(self.lastEmptyTS)),retain=True) self.client.publish(f'solarflow-hub/{self.deviceId}/control/batteryTarget',"charging",retain=True) @@ -360,8 +357,8 @@ def setBuzzer(self, state: bool): self.client.publish(self.property_topic,json.dumps(buzzer)) def setBypass(self, state: bool): - buzzer = {"properties": { "passMode": 2 if state else 1 }} - self.client.publish(self.property_topic,json.dumps(buzzer)) + passmode = {"properties": { "passMode": 2 if state else 1 }} + self.client.publish(self.property_topic,json.dumps(passmode)) if not state: self.bypass = state # required for cases where we can't wait on confirmation on turning bypass off