from dbus.exceptions import DBusException
from gi.repository import GLib
import logging
from math import pi, floor, ceil
import traceback
from itertools import count, chain
from functools import partial, wraps

# Victron packages
from sc_utils import safeadd, copy_dbus_value, reify, ExpiringValue
from ve_utils import exit_on_error

from delegates.base import SystemCalcDelegate
from delegates.batteryservice import BatteryService
from delegates.multi import Multi as MultiService

# Adjust things this often (in seconds)
# solar chargers has to switch to HEX mode each time we write a value to its
# D-Bus service. Writing too often may block text messages. In MPPT firmware
# v1.23 and later, all relevant values will be transmitted as asynchronous
# message, so the update rate could be increased. For now, keep this at 3 and
# above.
ADJUST = 3

VEBUS_FIRMWARE_REQUIRED = 0x422
VEDIRECT_FIRMWARE_REQUIRED = 0x129
VECAN_FIRMWARE_REQUIRED = 0x10200 # 1.02, 24-bit version

# This is a place to account for some BMS quirks where we may have to ignore
# the BMS value and substitute our own.

def _byd_quirk(dvcc, bms, charge_voltage, charge_current, feedback_allowed):
	""" Quirk for the BYD batteries. When the battery sends CCL=0, float it at
	   55V. """
	if charge_current == 0:
		return (55, 40, feedback_allowed, False)
	return (charge_voltage, charge_current, feedback_allowed, False)

def _lg_quirk(dvcc, bms, charge_voltage, charge_current, feedback_allowed):
	""" Quirk for LG batteries. The hard limit is 58V. Above that you risk
	    tripping on high voltage. The batteries publish a charge voltage of 57.7V
	    but we need to make room for an 0.4V overvoltage when feed-in is enabled.
	"""
	# Make room for a potential 0.4V at the top
	return (min(charge_voltage, 57.3), charge_current, feedback_allowed, False)

def _pylontech_quirk(dvcc, bms, charge_voltage, charge_current, feedback_allowed):
	""" Quirk for Pylontech. Make a bit of room at the top. Pylontech says that
	    at 51.8V the battery is 95% full, and that balancing starts at 90%.
	    53.2V is normally considered 100% full, and 54V raises an alarm. By
		running the battery at 52.4V it will be 99%-100% full, balancing should
		be active, and we should avoid high voltage alarms.

	    Identify 24-V batteries by the lower charge voltage, and do the same
	    thing with an 8-to-15 cell ratio, +-3.48V per cell.
	"""
	# Use 3.48V per cell plus a little, 52.4V for 15 cell 48V batteries.
	# Use 3.46V per cell plus a little, 27.8V for 24V batteries testing shows that's 100% SOC.
	# That leaves 1.6V margin for 48V batteries and 1.0V for 24V.
	# See https://github.com/victronenergy/venus/issues/536
	if charge_voltage > 55:
		# 48V battery (16 cells.) Assume BMS knows what it's doing.
		return (charge_voltage, charge_current, feedback_allowed, False)
	if charge_voltage > 30:
		# 48V battery (15 cells)
		return (min(charge_voltage, 52.4), charge_current, feedback_allowed, False)
	if charge_voltage > 20:
		# 24V battery (8 cells). 24V batteries send CCL=0 when they are full,
		# whereas the 48V batteries reduce CCL by 50% when the battery is full.
		# Do the same for 24V batteries. The normal limit is C/2, so put the
		# limit to C/4. Note that this is just a nicety, the important part is
		# to clip the charge voltage to 27.8 volts. That fixes the sawtooth
		# issue.
		capacity = bms.capacity or 55
		return (min(charge_voltage, 27.8), max(charge_current, round(capacity/4.0)), feedback_allowed, False)

	# Not known, probably a 12V battery.
	return (charge_voltage, charge_current, feedback_allowed, False)

def _pylontech_pelio_quirk(dvcc, bms, charge_voltage, charge_current, feedback_allowed):
	""" Quirk for Pelio-L batteries. This is a 16-cell battery. 56V is 3.5V per
	    cell which is where this battery registers 100% SOC. Battery sends
	    CCL=0 at 3.55V per cell, to ensure good feed-in of excess DC coupled
	    PV, set the lower limit to 20% of capacity, which is what the battery
	    itself imposes at around 98% SOC.
	"""
	capacity = bms.capacity or 100.0
	return (min(charge_voltage, 56.0), max(charge_current, round(capacity/5.0)), feedback_allowed, False)

def _lynx_smart_bms_quirk(dvcc, bms, charge_voltage, charge_current, feedback_allowed):
	""" When the Lynx Smart BMS sends CCL=0, it wants all chargers to stop. """
	return (charge_voltage, charge_current, feedback_allowed, True)

QUIRKS = {
	0xB004: _lg_quirk,
	0xB009: _pylontech_quirk,
	0xB00A: _byd_quirk,
	0xB015: _byd_quirk,
	0xB019: _byd_quirk,
	0xB029: _pylontech_pelio_quirk,
	0xA3E5: _lynx_smart_bms_quirk,
	0xA3E6: _lynx_smart_bms_quirk,
}

def distribute(current_values, max_values, increment):
	""" current_values and max_values are lists of equal size containing the
	    current limits, and the maximum they can be increased to. increment
	    contains the amount by which we want to increase the total, ie the sum
	    of the values in current_values, while staying below max_values.

	    This is done simply by first attempting to spread the increment
	    equally. If a value exceeds the max in that process, the remainder is
	    thrown back into the pot and distributed equally among the rest.

	    Negative values are also handled, and zero is assumed to be the
	    implicit lower limit. """
	n = cn = len(current_values)
	new_values = [-1] * n
	for j in range(0, n):
		for i, mv, av in zip(count(), max_values, current_values):
			assert mv >= 0
			if new_values[i] == mv or new_values[i] == 0:
				continue
			nv = av + float(increment) / cn

			if nv >= mv:
				increment += av - mv
				cn -= 1
				new_values[i] = mv
				break
			elif nv < 0:
				increment += av
				cn -= 1
				new_values[i] = 0
				break

			new_values[i] = nv
		else:
			break
		continue
	return new_values

class LowPassFilter(object):
	""" Low pass filter, with a cap. """
	def __init__(self, omega, value):
		self.omega = omega
		self._value = value

	def update(self, newvalue):
		self._value += (newvalue - self._value) * self.omega
		return self._value

	@property
	def value(self):
		return self._value

class BaseCharger(object):
	def __init__(self, monitor, service):
		self.monitor = monitor
		self.service = service

	def _get_path(self, path):
		return self.monitor.get_value(self.service, path)

	def _set_path(self, path, v):
		if self.monitor.seen(self.service, path):
			self.monitor.set_value_async(self.service, path, v)

	@property
	def firmwareversion(self):
		return self.monitor.get_value(self.service, '/FirmwareVersion')

	@property
	def product_id(self):
		return self.monitor.get_value(self.service, '/ProductId') or 0

	@property
	def chargecurrent(self):
		return self._get_path('/Dc/0/Current')

	@property
	def n2k_device_instance(self):
		return self.monitor.get_value(self.service, '/N2kDeviceInstance')

	@property
	def connection(self):
		return self._get_path('/Mgmt/Connection')

	@property
	def active(self):
		return self._get_path('/State') != 0

	@property
	def has_externalcontrol_support(self):
		""" Override this to implement detection of external control support.
		"""
		return False

	@property
	def want_bms(self):
		""" Indicates whether this solar charger was previously
		    controlled by a BMS and therefore expects one to
		    be present. """
		return 0

	@property
	def maxchargecurrent(self):
		v = self._get_path('/Link/ChargeCurrent')
		return v if v is not None else self.currentlimit

	@maxchargecurrent.setter
	def maxchargecurrent(self, v):
		v = max(0, min(v, self.currentlimit))
		self._set_path('/Link/ChargeCurrent', v)

	@property
	def chargevoltage(self):
		return self._get_path('/Link/ChargeVoltage')

	@chargevoltage.setter
	def chargevoltage(self, v):
		self._set_path('/Link/ChargeVoltage', v)

	@property
	def currentlimit(self):
		return self._get_path('/Settings/ChargeCurrentLimit')

	def maximize_charge_current(self):
		""" Max out the charge current of this solar charger by setting
		    ChargeCurrent to the configured limit in settings. """
		if self.monitor.seen(self.service, '/Link/ChargeCurrent'):
			copy_dbus_value(self.monitor,
				self.service, '/Settings/ChargeCurrentLimit',
				self.service, '/Link/ChargeCurrent')

	@property
	def smoothed_current(self):
		# For chargers that are not solar-chargers, the generated current
		# should be fairly stable already
		return self.chargecurrent or 0

class Networkable(object):
	""" Mix into BaseCharger to support network paths. """
	@property
	def networkmode(self):
		return self._get_path('/Link/NetworkMode')

	@networkmode.setter
	def networkmode(self, v):
		self._set_path('/Link/NetworkMode', 0)
		self._set_path('/Settings/BmsPresent',0)

class SolarCharger(BaseCharger, Networkable):
	""" Encapsulates a solar charger on dbus. Exposes dbus paths as convenient
	    attributes. """

	def __init__(self, monitor, service):
		super().__init__(monitor, service)
		self._smoothed_current = LowPassFilter((2 * pi)/20, self.chargecurrent or 0)
		self._has_externalcontrol_support = False

	@property
	def has_externalcontrol_support(self):
		# If we have previously determined that there is support, re-use that.
		# If the firmware is ever to be downgraded, the solarcharger must necessarily
		# disconnect and reconnect, so this is completely safe.
		if self._has_externalcontrol_support:
			return True

		# These products are known to have support, but may have older firmware
		# See https://github.com/victronenergy/venus/issues/655
		if 0xA102 <= self.product_id <= 0xA10E:
			self._has_externalcontrol_support = True
			return True

		v = self.firmwareversion

		# If the firmware version is not known, don't raise a false
		# warning.
		if v is None:
			return True

		# New VE.Can controllers have 24-bit version strings. One would
		# hope that any future VE.Direct controllers with 24-bit firmware
		# versions will 1) have a version larger than 1.02 and 2) support
		# external control.
		if v & 0xFF0000:
			self._has_externalcontrol_support = (v >= VECAN_FIRMWARE_REQUIRED)
		else:
			self._has_externalcontrol_support = (v >= VEDIRECT_FIRMWARE_REQUIRED)
		return self._has_externalcontrol_support

	@property
	def smoothed_current(self):
		""" Returns the internal low-pass filtered current value. """
		return self._smoothed_current.value

	def update_values(self):
		# This is called periodically from a timer to maintain
		# a smooth current value.
		v = self.monitor.get_value(self.service, '/Dc/0/Current')
		if v is not None:
			self._smoothed_current.update(v)

class Alternator(BaseCharger, Networkable):
	""" This also includes other DC/DC converters. """
	@property
	def has_externalcontrol_support(self):
		# If it has the ChargeCurrent path, we assume it has
		# external control support
		return self.monitor.seen(self.service, '/Link/ChargeCurrent')

class InverterCharger(SolarCharger):
	""" Encapsulates an inverter/charger object, currently the inverter RS,
	    which has a solar input and can charge the battery like a solar
	    charger, but is also an inverter.
	"""
	def __init__(self, monitor, service):
		super(InverterCharger, self).__init__(monitor, service)

	@property
	def has_externalcontrol_support(self):
		# Inverter RS always had support
		return True

	@property
	def maxdischargecurrent(self):
		""" Returns discharge current setting. This does nothing except
		    return the previously set value. """
		return self.monitor.get_value(self.service, '/Link/DischargeCurrent')

	@maxdischargecurrent.setter
	def maxdischargecurrent(self, limit):
		self.monitor.set_value_async(self.service, '/Link/DischargeCurrent', limit)

	def set_maxdischargecurrent(self, limit):
		""" Write the maximum discharge limit across. The firmware
		    already handles a zero by turning off. """
		self.maxdischargecurrent = limit

	@property
	def active(self):
		# The charger part is active, as long as the maximum charging
		# power value is more than zero.
		return (self.monitor.get_value(self.service,
			'/Settings/ChargeCurrentLimit') or 0) > 0

class InverterSubsystem(object):
	""" Encapsulate collection of inverters. """
	def __init__(self, monitor):
		self.monitor = monitor
		self._inverters = {}

	def _add_inverter(self, ob):
		self._inverters[ob.service] = ob
		return ob

	def remove_inverter(self, service):
		del self._inverters[service]

	def __iter__(self):
		return iter(self._inverters.values())

	def __len__(self):
		return len(self._inverters)

	def __contains__(self, k):
		return k in self._inverters

	def set_maxdischargecurrent(self, limit):
		# Inverters only care about limit=0, so simply send
		# it to all.
		for inverter in self:
			inverter.set_maxdischargecurrent(limit)

class ChargerSubsystem(object):
	""" Encapsulates a collection of chargers or devices that incorporate a
	    charger, to collectively make up a charging system (sans Multi).
	    Properties related to the whole system or some combination of the
	    individual chargers are exposed here as attributes. """
	def __init__(self, monitor):
		self.monitor = monitor
		self._solarchargers = {}
		self._otherchargers = {}

	def add_solar_charger(self, service):
		self._solarchargers[service] = charger = SolarCharger(self.monitor, service)
		return charger

	def add_alternator(self, service):
		self._otherchargers[service] = charger = Alternator(self.monitor, service)
		return charger

	def add_invertercharger(self, service):
		self._solarchargers[service] = inverter = InverterCharger(self.monitor, service)
		return inverter

	def remove_charger(self, service):
		for di in (self._solarchargers, self._otherchargers):
			try:
				del di[service]
			except KeyError:
				pass

	def __iter__(self):
		return iter(chain(self._solarchargers.values(), self._otherchargers.values()))

	def __len__(self):
		return len(self._solarchargers) + len(self._otherchargers)

	def __contains__(self, k):
		return k in self._solarchargers or k in self._otherchargers

	@property
	def has_externalcontrol_support(self):
		# Only consider solarchargers. This is used for firmware warning
		# above, and we only care about the solar chargers there.
		return all(s.has_externalcontrol_support for s in self._solarchargers.values())

	@property
	def has_vecan_chargers(self):
		""" Returns true if we have any VE.Can chargers in the system. This is
		    used elsewhere to enable broadcasting charge voltages on the relevant
		    can device. """
		return any((s.connection == 'VE.Can' for s in self))

	@property
	def want_bms(self):
		""" Return true if any of our solar chargers expect a BMS to
		    be present. """
		return any((s.want_bms for s in self))

	@property
	def totalcapacity(self):
		""" Total capacity if all chargers are running at full power. """
		return safeadd(*(c.currentlimit for c in self))

	@property
	def smoothed_current(self):
		""" Total smoothed current, calculated by adding the smoothed current
		    of the individual chargers. """
		return safeadd(*(c.smoothed_current for c in self)) or 0

	@property
	def solar_current(self):
		return safeadd(*(c.smoothed_current for c in self._solarchargers.values())) or 0

	def maximize_charge_current(self):
		""" Max out all chargers. """
		for charger in self:
			charger.maximize_charge_current()

	def shutdown_chargers(self):
		""" Shut down all chargers. """
		for charger in self:
			charger.maxchargecurrent = 0

	def set_networked(self, has_bms, bms_charge_voltage, charge_voltage, max_charge_current, feedback_allowed, stop_on_mcc0):
		""" This is the main entry-point into the solar charger subsystem. This
		    sets all chargers to the same charge_voltage, and distributes
		    max_charge_current between the chargers. If feedback_allowed, then
		    we simply max out the chargers. We also don't bother with
		    distribution if there's only one charger in the system or if
		    it exceeds our total capacity.
		"""
		# Network mode:
		# bit 0: Operated in network environment
		# bit 2: Remote Hub-1 control (MPPT will accept charge voltage and max charge current)
		# bit 3: Remote BMS control (MPPT enter BMS mode)
		network_mode = 1 | (0 if charge_voltage is None and max_charge_current is None else 4) | (8 if has_bms else 0)
		network_mode_written = False
		for charger in self:
			charger.networkmode = network_mode
			network_mode_written = True

		# Distribute the voltage setpoint to all solar chargers.
		# Non-solar chargers are controlled elsewhere.
		voltage_written = 0
		if charge_voltage is not None:
			voltage_written = int(len(self)>0)
			for charger in self._solarchargers.values():
				charger.chargevoltage = charge_voltage

		# Distribute the original BMS voltage setpoint, if there is one,
		# to the other chargers
		if bms_charge_voltage is not None:
			for charger in self._otherchargers.values():
				charger.chargevoltage = bms_charge_voltage

		# Do not limit max charge current when feedback is allowed. The
		# rationale behind this is that MPPT charge power should match the
		# capabilities of the battery. If the default charge algorithm is used
		# by the MPPTs, the charge current should stay within limits. This
		# avoids a problem that we do not know if extra MPPT power will be fed
		# back to the grid when we decide to increase the MPPT max charge
		# current.
		#
		# Additionally, don't bother with chargers that are disconnected.
		chargers = [x for x in self._solarchargers.values() if x.active and x.maxchargecurrent is not None and x.n2k_device_instance in (0, None)]
		if len(chargers) > 0:
			if stop_on_mcc0 and max_charge_current == 0:
				self.shutdown_chargers()
			elif feedback_allowed:
				self.maximize_charge_current()
			elif max_charge_current is not None:
				if len(chargers) == 1:
					# The simple case: Only one charger. Simply assign the
					# limit to the charger
					sc = chargers[0]
					cc = min(ceil(max_charge_current), sc.currentlimit)
					sc.maxchargecurrent = cc
				elif max_charge_current > self.totalcapacity * 0.95:
					# Another simple case, we're asking for more than our
					# combined capacity (with a 5% margin)
					self.maximize_charge_current()
				else:
					# The hard case, we have more than one CC and we want
					# less than our capacity.
					self._distribute_current(chargers, max_charge_current)

		# Split remainder over other chargers, according to individual
		# capacity. Only consider controllable devices.
		if max_charge_current is not None:
			remainder = max(0.0, max_charge_current - self.solar_current)
			controllable = [c for c in self._otherchargers.values() if c.has_externalcontrol_support]
			capacity = safeadd(*(c.currentlimit for c in controllable)) or 0
			if capacity > 0:
				for c in controllable:
					c.maxchargecurrent = remainder * c.currentlimit / capacity

		# Return flags of what we did
		return voltage_written, int(network_mode_written and max_charge_current is not None), network_mode

	# The math for the below is as follows. Let c be the total capacity of the
	# charger, l be the current limit, a the actual current it produces, k the
	# total current limit for the two chargers, and m the margin (l - a)
	# between the limit and what is produced.
	#
	# We want m/c to be the same for all our chargers.
	#
	# Expression 1: (l1-a1)/c1 == (l2-a2)/c2
	# Expression 2: l1 + l2 == k
	#
	# Solving that yields the expression below.
	@staticmethod
	def _balance_chargers(charger1, charger2, l1, l2):
		c1, c2 = charger1.currentlimit, charger2.currentlimit
		a1 = min(charger1.smoothed_current, c1)
		a2 = min(charger2.smoothed_current, c2)
		k = l1 + l2

		try:
			l1 = round((c2 * a1 - c1 * a2 + k * c1)/(c1 + c2), 1)
		except ArithmeticError:
			return l1, l2 # unchanged
		else:
			l1 = max(min(l1, c1), 0)
			return l1, k - l1

	@staticmethod
	def _distribute_current(chargers, max_charge_current):
		""" This is called if there are two or more solar chargers. It
		    distributes the charge current over all of them. """

		# Take the difference between the values and spread it over all
		# the chargers. The maxchargecurrents of the chargers should ideally
		# always add up to the whole.
		limits = [c.maxchargecurrent for c in chargers]
		ceilings = [c.currentlimit for c in chargers]

		# We cannot have a max_charge_current higher than the sum of the
		# ceilings.
		max_charge_current = min(sum(ceilings), max_charge_current)


		# Check how far we have to move our adjustment. If it doesn't have to
		# move much (or at all), then just balance the charge limits. Our
		# threshold for doing an additional distribution of charge is relative
		# to the number of chargers, as it makes no sense to attempt a
		# distribution if there is too little to be gained. The chosen value
		# here is 100mA per charger.
		delta = max_charge_current - sum(limits)
		if abs(delta) > 0.1 * len(chargers):
			limits = distribute(limits, ceilings, delta)
			for charger, limit in zip(chargers, limits):
				charger.maxchargecurrent = limit
		else:
			# Balance the limits so they have the same headroom at the top.
			# Each charger is balanced against its neighbour, the one at the
			# end is paired with the one at the start.
			limits = []
			r = chargers[0].maxchargecurrent
			for c1, c2 in zip(chargers, chargers[1:]):
				l, r = ChargerSubsystem._balance_chargers(c1, c2, r, c2.maxchargecurrent)
				limits.append(l)
			l, limits[0] = ChargerSubsystem._balance_chargers(c2, chargers[0], r, limits[0])
			limits.append(l)

			for charger, limit in zip(chargers, limits):
				charger.maxchargecurrent = limit

	def update_values(self):
		# This is called periodically from a timer to update contained
		# solar chargers with values that they track.
		for charger in self:
			try:
				charger.update_values()
			except AttributeError:
				pass

class BatteryOperationalLimits(object):
	""" Only used to encapsulate this part of the Multi's functionality.
	"""
	def __init__(self, multi):
		self._multi = multi

	def _property(path, self):
		# Due to the use of partial, path and self is reversed.
		return self._multi.monitor.get_value(self._multi.service, path)

	def _set_property(path, self, v):
		# None of these values can be negative
		if v is not None:
			v = max(0, v)
		self._multi.monitor.set_value_async(self._multi.service, path, v)

	chargevoltage = property(
		partial(_property, '/BatteryOperationalLimits/MaxChargeVoltage'),
		partial(_set_property, '/BatteryOperationalLimits/MaxChargeVoltage'))
	maxchargecurrent = property(
		partial(_property, '/BatteryOperationalLimits/MaxChargeCurrent'),
		partial(_set_property, '/BatteryOperationalLimits/MaxChargeCurrent'))
	maxdischargecurrent = property(
		partial(_property, '/BatteryOperationalLimits/MaxDischargeCurrent'),
		partial(_set_property, '/BatteryOperationalLimits/MaxDischargeCurrent'))
	batterylowvoltage = property(
		partial(_property, '/BatteryOperationalLimits/BatteryLowVoltage'),
		partial(_set_property, '/BatteryOperationalLimits/BatteryLowVoltage'))


class Multi(object):
	""" Encapsulates the multi. Makes access to dbus paths a bit neater by
	    exposing them as attributes. """
	def __init__(self, monitor, service):
		self.monitor = monitor
		self._service = service
		self.bol = BatteryOperationalLimits(self)
		self._dc_current = LowPassFilter((2 * pi)/30, 0)
		self._v = object()

	@property
	def service(self):
		return getattr(MultiService.instance.vebus_service, 'service', None)

	@property
	def active(self):
		return self.service is not None

	@property
	def ac_connected(self):
		return self.monitor.get_value(self.service, '/Ac/ActiveIn/Connected') == 1

	@property
	def has_bolframe(self):
		return self.monitor.get_value(self.service, '/FirmwareFeatures/BolFrame') == 1

	@property
	def has_ess_assistant(self):
		# We do not analyse the content of /Devices/0/Assistants, because that
		# would require us to keep a list of ESS assistant version numbers (see
		# VebusSocWriter._hub2_assistant_ids). Because that list is expected to
		# change (unlike the list of hub-2 assistants), we use
		# /Hub4/AssistantId to check the presence. It is guaranteed that
		# /Hub4/AssistantId will be published before /Devices/0/Assistants.
		assistants = self.monitor.get_value(self.service, '/Devices/0/Assistants')
		return assistants is not None and \
			self.monitor.get_value(self.service, '/Hub4/AssistantId') == 5

	@property
	def dc_current(self):
		""" Return a low-pass smoothed current. """
		return self._dc_current.value

	@property
	def hub_voltage(self):
		return self.monitor.get_value(self.service, '/Hub/ChargeVoltage')

	@property
	def maxchargecurrent(self):
		return self.monitor.get_value(self.service, '/Dc/0/MaxChargeCurrent')

	@maxchargecurrent.setter
	def maxchargecurrent(self, v):
		# If the Multi is not ready, don't write to it just yet
		if self.active and self.maxchargecurrent is not None and v != self._v:
			# The maximum present charge current is 6-parallel 12V 5kva units, 6*220 = 1320A.
			# We will consider 10000A to be impossibly high.
			self.monitor.set_value_async(self.service, '/Dc/0/MaxChargeCurrent', 10000 if v is None else v)
			self._v = v

	@property
	def state(self):
		return self.monitor.get_value(self.service, '/State')

	@property
	def feedin_enabled(self):
		return self.monitor.get_value(self.service,
			'/Hub4/L1/DoNotFeedInOvervoltage') == 0

	@property
	def firmwareversion(self):
		return self.monitor.get_value(self.service, '/FirmwareVersion')

	@property
	def allow_to_charge(self):
		return self.monitor.get_value(self.service, '/Bms/AllowToCharge') != 0

	@property
	def has_vebus_bms(self):
		""" This checks that we have a VE.Bus BMS. """
		return self.monitor.get_value(self.service, '/Bms/BmsType') == 2

	@property
	def has_vebus_bmsv2(self):
		""" Checks that we have v2 of the VE.Bus BMS, but also that we can
		    properly use it, that is we also have an mk3. """
		version = self.monitor.get_value(self.service, '/Devices/Bms/Version')
		atc = self.monitor.get_value(self.service, '/Bms/AllowToCharge')

		# If AllowToCharge is defined, but we have no version, then the Multi
		# is off, but we still have a v2 BMS. V1 goes invalid if the multi
		# is off. Yes, this is kludgy, but it is less kludgy than the
		# fix the other end would require.
		if self.has_vebus_bms and atc is not None and version is None:
			return True

		# Otherwise, if the Multi is on, check the version to see if we should
		# enable v2 functionality.
		return (version or 0) >= 1146100 and \
			self.monitor.get_value(self.service, '/Interfaces/Mk2/ProductName') == 'MK3'

	def update_values(self, limit):
		c = self.monitor.get_value(self.service, '/Dc/0/Current', 0)
		if c is not None:
			# Cap the filter at a limit. If we don't do this, dc currents
			# in excess of our capacity causes a kind of wind-up that delays
			# backing-off when the load drops suddenly.
			if limit is not None:
				c = max(c, -limit)
			self._dc_current.update(c)

class Dvcc(SystemCalcDelegate):
	""" This is the main DVCC delegate object. """
	def __init__(self, sc):
		super(Dvcc, self).__init__()
		self.systemcalc = sc
		self._chargesystem = None
		self._vecan_services = []
		self._timer = None
		self._tickcount = ADJUST
		self._dcsyscurrent = LowPassFilter((2 * pi)/20, 0.0)
		self._internal_mcp = ExpiringValue(3, None) # Max charging power

	def get_input(self):
		return [
			('com.victronenergy.vebus', [
				'/Ac/ActiveIn/Connected',
				'/Hub/ChargeVoltage',
				'/Dc/0/Current',
				'/Dc/0/MaxChargeCurrent',
				'/State',
				'/BatteryOperationalLimits/BatteryLowVoltage',
				'/BatteryOperationalLimits/MaxChargeCurrent',
				'/BatteryOperationalLimits/MaxChargeVoltage',
				'/BatteryOperationalLimits/MaxDischargeCurrent',
				'/Bms/AllowToCharge',
				'/Bms/BmsType',
				'/Devices/Bms/Version',
				'/FirmwareFeatures/BolFrame',
				'/Hub4/L1/DoNotFeedInOvervoltage',
				'/FirmwareVersion',
				'/Interfaces/Mk2/ProductName']),
			('com.victronenergy.solarcharger', [
				'/ProductId',
				'/Dc/0/Current',
				'/Link/NetworkMode',
				'/Link/ChargeVoltage',
				'/Link/ChargeCurrent',
				'/Settings/ChargeCurrentLimit',
				'/State',
				'/FirmwareVersion',
				'/N2kDeviceInstance',
				'/Mgmt/Connection',
				'/Settings/BmsPresent']),
			('com.victronenergy.alternator', [
				'/ProductId',
				'/Dc/0/Voltage',
				'/Dc/0/Current',
				'/Link/NetworkMode',
				'/Link/ChargeVoltage',
				'/Link/ChargeCurrent',
				'/Settings/ChargeCurrentLimit',
				'/State',
				'/FirmwareVersion',
				'/N2kDeviceInstance',
				'/Mgmt/Connection',
				'/Settings/BmsPresent']),
			('com.victronenergy.inverter', [
				'/ProductId',
				'/Dc/0/Current',
				'/IsInverterCharger',
				'/Link/NetworkMode',
				'/Link/ChargeVoltage',
				'/Link/ChargeCurrent',
				'/Link/DischargeCurrent',
				'/Settings/ChargeCurrentLimit',
				'/State',
				'/N2kDeviceInstance',
				'/Mgmt/Connection',
				'/Settings/BmsPresent']),
			('com.victronenergy.multi', [
				'/ProductId',
				'/Dc/0/Current',
				'/IsInverterCharger',
				'/Link/ChargeCurrent',
				'/Link/DischargeCurrent',
				'/Settings/ChargeCurrentLimit',
				'/State',
				'/N2kDeviceInstance',
				'/Mgmt/Connection',
				'/Settings/BmsPresent']),
			('com.victronenergy.vecan',	[
				'/Link/ChargeVoltage',
				'/Link/NetworkMode']),
			('com.victronenergy.settings', [
				 '/Settings/CGwacs/OvervoltageFeedIn',
				 '/Settings/Services/Bol'])]

	def get_settings(self):
		return [
			('maxchargecurrent', '/Settings/SystemSetup/MaxChargeCurrent', -1, -1, 10000),
			('maxchargevoltage', '/Settings/SystemSetup/MaxChargeVoltage', 0.0, 0.0, 80.0),
			('bol', '/Settings/Services/Bol', 0, 0, 7)
		]

	def set_sources(self, dbusmonitor, settings, dbusservice):
		SystemCalcDelegate.set_sources(self, dbusmonitor, settings, dbusservice)
		self._chargesystem = ChargerSubsystem(dbusmonitor)
		self._inverters = InverterSubsystem(dbusmonitor)
		self._multi = Multi(dbusmonitor, dbusservice)

		self._dbusservice.add_path('/Control/SolarChargeVoltage', value=0)
		self._dbusservice.add_path('/Control/SolarChargeCurrent', value=0)
		self._dbusservice.add_path('/Control/EffectiveChargeVoltage', value=None)
		self._dbusservice.add_path('/Control/BmsParameters', value=0)
		self._dbusservice.add_path('/Control/MaxChargeCurrent', value=0)
		self._dbusservice.add_path('/Control/Dvcc', value=self.has_dvcc)
		self._dbusservice.add_path('/Debug/BatteryOperationalLimits/SolarVoltageOffset', value=0, writeable=True)
		self._dbusservice.add_path('/Debug/BatteryOperationalLimits/VebusVoltageOffset', value=0, writeable=True)
		self._dbusservice.add_path('/Debug/BatteryOperationalLimits/CurrentOffset', value=0, writeable=True)
		self._dbusservice.add_path('/Dvcc/Alarms/FirmwareInsufficient', value=0)
		self._dbusservice.add_path('/Dvcc/Alarms/MultipleBatteries', value=0)

	def device_added(self, service, instance, do_service_change=True):
		service_type = service.split('.')[2]
		if service_type == 'solarcharger':
			self._chargesystem.add_solar_charger(service)
		elif service_type in ('inverter', 'multi'):
			if self._dbusmonitor.get_value(service, '/IsInverterCharger') == 1:
				# Add to both the solarcharger and inverter collections.
				# add_invertercharger returns an object that can be directly
				# added to the inverter collection.
				self._inverters._add_inverter(
					self._chargesystem.add_invertercharger(service))
		elif service_type == 'vecan':
			self._vecan_services.append(service)
		elif service_type == 'alternator':
			self._chargesystem.add_alternator(service)
		elif service_type == 'battery':
			pass # install timer below
		else:
			# Skip timer code below
			return

		if self._timer is None:
			self._timer = GLib.timeout_add(1000, exit_on_error, self._on_timer)

	def device_removed(self, service, instance):
		if service in self._chargesystem:
			self._chargesystem.remove_charger(service)
			# Some solar chargers are inside an inverter
			if service in self._inverters:
				self._inverters.remove_inverter(service)
		elif service in self._vecan_services:
			self._vecan_services.remove(service)
		elif service in self._inverters:
			self._inverters.remove_inverter(service)
		if len(self._chargesystem) == 0 and len(self._vecan_services) == 0 and \
			len(BatteryService.instance.batteries) == 0 and self._timer is not None:
			GLib.source_remove(self._timer)
			self._timer = None

	def _property(path, self):
		# Due to the use of partial, path and self is reversed.
		try:
			return float(self._dbusservice[path])
		except ValueError:
			return None

	solarvoltageoffset = property(partial(_property, '/Debug/BatteryOperationalLimits/SolarVoltageOffset'))
	invertervoltageoffset = property(partial(_property, '/Debug/BatteryOperationalLimits/VebusVoltageOffset'))
	currentoffset = property(partial(_property, '/Debug/BatteryOperationalLimits/CurrentOffset'))

	@property
	def internal_maxchargepower(self):
		return self._internal_mcp.get()

	@internal_maxchargepower.setter
	def internal_maxchargepower(self, v):
		self._internal_mcp.set(v)

	@property
	def dcsyscurrent(self):
		""" Return non-zero DC system current, if it is based on
		    a real measurement. If an estimate/calculation, we cannot use it.
		"""
		if self._dbusservice['/Dc/System/MeasurementType'] == 1:
			try:
				v = self._dbusservice['/Dc/Battery/Voltage']
				return self._dcsyscurrent.update(
					float(self._dbusservice['/Dc/System/Power'])/v)
			except (TypeError, ZeroDivisionError):
				pass
		return 0.0

	@property
	def has_ess_assistant(self):
		return self._multi.active and self._multi.has_ess_assistant

	@property
	def has_dvcc(self):
		# 0b00  = Off
		# 0b01  = On
		# 0b10  = Forced off
		# 0b11  = Forced on
		v = self._settings['bol']
		return bool(v & 1)

	@property
	def bms(self):
		return BatteryService.instance.bms

	@property
	def bms_seen(self):
		return self._chargesystem.want_bms

	def _on_timer(self):
		def update_solarcharger_control_flags(voltage_written, current_written, chargevoltage):
			self._dbusservice['/Control/SolarChargeVoltage'] = voltage_written
			self._dbusservice['/Control/SolarChargeCurrent'] = current_written
			self._dbusservice['/Control/EffectiveChargeVoltage'] = chargevoltage

		bol_support = self.has_dvcc

		self._tickcount -= 1; self._tickcount %= ADJUST

		if not bol_support:
			if self._tickcount > 0: return True

			voltage_written, current_written = self._legacy_update_solarchargers()
			update_solarcharger_control_flags(voltage_written, current_written, None) # Not tracking for non-DVCC case
			self._dbusservice['/Control/BmsParameters'] = 0
			self._dbusservice['/Control/MaxChargeCurrent'] = 0
			self._dbusservice['/Control/Dvcc'] = 0
			self._dbusservice['/Dvcc/Alarms/FirmwareInsufficient'] = 0
			self._dbusservice['/Dvcc/Alarms/MultipleBatteries'] = 0
			return True


		# BOL/DVCC support below
		self._dbusservice['/Dvcc/Alarms/FirmwareInsufficient'] = int(
			not self._chargesystem.has_externalcontrol_support or (
			self._multi.firmwareversion is not None and self._multi.firmwareversion < VEBUS_FIRMWARE_REQUIRED))
		self._dbusservice['/Dvcc/Alarms/MultipleBatteries'] = int(
			len(BatteryService.instance.bmses) > 1)

		# Update subsystems
		self._chargesystem.update_values()
		self._multi.update_values(self._chargesystem.totalcapacity)

		# Below are things we only do every ADJUST seconds
		if self._tickcount > 0: return True

		# Signal Dvcc support to other processes
		self._dbusservice['/Control/Dvcc'] = 1

		# Check that we have not lost the BMS, if we ever had one.  If the BMS
		# is lost, stop passing information to the solar chargers so that they
		# might time out.
		bms_service = self.bms
		if self.bms_seen and bms_service is None and not self._multi.has_vebus_bmsv2:
			# BMS is lost
			update_solarcharger_control_flags(0, 0, None)
			return True

		# Get the user current limit, if set
		user_max_charge_current = self._settings['maxchargecurrent']
		if user_max_charge_current < 0: user_max_charge_current = None

		# If there is a BMS, get the charge voltage and current from it
		max_charge_current = None
		charge_voltage = None
		feedback_allowed = self.feedback_allowed
		stop_on_mcc0 = False
		has_bms = bms_service is not None
		if has_bms:
			charge_voltage, max_charge_current, feedback_allowed, stop_on_mcc0 = \
				self._adjust_battery_operational_limits(bms_service, feedback_allowed)

		# Check /Bms/AllowToCharge on the VE.Bus service, and set
		# max_charge_current to zero if charging is not allowed.  Skip this if
		# ESS is involved, then the Multi controls this through the charge
		# voltage. If it is BMS v2, then also set BMS bit so that solarchargers
		# go into #67 if we lose it.
		if self._multi.has_vebus_bms:
			stop_on_mcc0 = True
			has_bms = has_bms or self._multi.has_vebus_bmsv2
			max_charge_current = 10000 if self._multi.allow_to_charge else 0

		# Take the lesser of the BMS and user current limits, wherever they exist
		maximae = [x for x in (user_max_charge_current, max_charge_current) if x is not None]
		max_charge_current = min(maximae) if maximae else None

		# Override the battery charge voltage by taking the lesser of the
		# voltage limits. Only override if the battery supplies one, to prevent
		# a voltage being sent to a Multi in a system without a managed battery.
		# Otherwise the Multi will go into passthru if the user disables this.
		if charge_voltage is not None:
			user_charge_voltage = self._settings['maxchargevoltage']
			if user_charge_voltage > 0:
				charge_voltage = min(charge_voltage, user_charge_voltage)

		# @todo EV What if ESS + OvervoltageFeedIn? In that case there is no
		# charge current control on the MPPTs, but we'll still indicate that
		# the control is active here. Should we?
		self._dbusservice['/Control/MaxChargeCurrent'] = \
			not self._multi.active or self._multi.has_bolframe

		# If there is a measured DC system, the Multi and solarchargers
		# should add extra current for that. Round this to nearest 100mA.
		if max_charge_current is not None and max_charge_current > 0 and not stop_on_mcc0:
			max_charge_current = round(max_charge_current + self.dcsyscurrent, 1)

		# We need to keep a copy of the original value for later. We will be
		# modifying one of them to compensate for vebus current.
		_max_charge_current = max_charge_current

		# If we have vebus current, we have to compensate for it. But if we must
		# stop on MCC=0, then only if the max charge current is above zero.
		# Otherwise leave it unmodified so that the solarchargers are also
		# stopped.
		vebus_dc_current = self._multi.dc_current
		if _max_charge_current is not None and vebus_dc_current is not None and \
				(not stop_on_mcc0 or _max_charge_current > 0) and vebus_dc_current < 0:
			_max_charge_current = ceil(_max_charge_current - vebus_dc_current)

		# Try to push the solar chargers to the vebus-compensated value
		voltage_written, current_written, effective_charge_voltage = \
			self._update_solarchargers_and_vecan(has_bms, charge_voltage,
			_max_charge_current, feedback_allowed, stop_on_mcc0)
		update_solarcharger_control_flags(voltage_written, current_written, effective_charge_voltage)

		# The Multi gets the remainder after subtracting what the solar
		# chargers made. If there is a maximum charge power from another
		# delegate (dynamicess), apply that here.
		if max_charge_current is not None:
			max_charge_current = max(0.0, round(max_charge_current - self._chargesystem.smoothed_current))

		try:
			internal_mcc = self.internal_maxchargepower / self._dbusservice['/Dc/Battery/Voltage']
		except (TypeError, ZeroDivisionError, ValueError):
			pass
		else:
			try:
				max_charge_current = min(x for x in (ceil(internal_mcc), max_charge_current) if x is not None)
			except ValueError:
				pass

		# Write the remainder to the Multi.
		# There are two ways to limit the charge current of a VE.Bus system. If we have a BMS,
		# the BOL parameter is used.
		# If not, then the BOL parameters are not available, and the /Dc/0/MaxChargeCurrent path is
		# used instead. This path relates to the MaxChargeCurrent setting as also available in
		# VEConfigure, except that writing to it only changes the value in RAM in the Multi.
		# Unlike VEConfigure it's not necessary to take the number of units in a system into account.
		#
		# Venus OS v2.30 fixes in mk2-dbus related to /Dc/0/MaxChargeCurrent:
		# 1) Fix charge current too high in systems with multiple units per phase. mk2-bus was dividing
		#    the received current only by the number of phases in the system instead of dividing by the
		#    number of units in the system.
		# 2) Fix setted charge current still active after disabling the "Limit charge current" setting.
		#    It used to be necessary to set a high current; and only then disable the setting or reset
		#    the VE.Bus system to re-initialise from the stored setting as per VEConfigure.
		bms_parameters_written = 0
		if bms_service is None:
			if max_charge_current is None:
				self._multi.maxchargecurrent = None
			else:
				# Don't bother setting a charge current at 1A or less
				self._multi.maxchargecurrent = max_charge_current if max_charge_current > 1 else 0
		else:
			bms_parameters_written = self._update_battery_operational_limits(bms_service, charge_voltage, max_charge_current)
		self._dbusservice['/Control/BmsParameters'] = int(bms_parameters_written or (bms_service is not None and voltage_written))

		return True

	def _adjust_battery_operational_limits(self, bms_service, feedback_allowed):
		""" Take the charge voltage and maximum charge current from the BMS
		    and adjust it as necessary. For now we only implement quirks
		    for batteries known to have them.
		"""
		cv = bms_service.chargevoltage
		mcc = bms_service.maxchargecurrent

		quirk = QUIRKS.get(bms_service.product_id)
		stop_on_mcc0 = False
		if quirk is not None:
			# If any quirks are registered for this battery, use that
			# instead.
			cv, mcc, feedback_allowed, stop_on_mcc0 = quirk(self, bms_service, cv, mcc, feedback_allowed)

		# Add debug offsets
		if cv is not None:
			cv = safeadd(cv, self.invertervoltageoffset)
		if mcc is not None:
			mcc = safeadd(mcc, self.currentoffset)
		return cv, mcc, feedback_allowed, stop_on_mcc0

	def _update_battery_operational_limits(self, bms_service, cv, mcc):
		""" This function writes the bms parameters across to the Multi
		    if it exists. Also communicate DCL=0 to inverters. """
		written = 0
		if self._multi.active:
			if cv is not None:
				self._multi.bol.chargevoltage = cv

			if mcc is not None:
				self._multi.bol.maxchargecurrent = mcc
				# Also set the maxchargecurrent, to ensure this is not stuck
				# at some lower value that overrides the intent here.
				try:
					self._multi.maxchargecurrent = max(self._multi.maxchargecurrent, mcc)
				except TypeError:
					pass

			# Copy the rest unmodified
			self._multi.bol.maxdischargecurrent = bms_service.maxdischargecurrent
			self._multi.bol.batterylowvoltage = bms_service.batterylowvoltage
			written = 1

		# Also control inverters if BMS stops discharge
		if len(self._inverters):
			self._inverters.set_maxdischargecurrent(bms_service.maxdischargecurrent)
			written = 1

		return written

	@property
	def feedback_allowed(self):
		# Feedback allowed is defined as 'ESS present and FeedInOvervoltage is
		# enabled'. This ignores other setups which allow feedback: hub-1.
		return self.has_ess_assistant and self._multi.ac_connected and \
			self._dbusmonitor.get_value('com.victronenergy.settings',
				'/Settings/CGwacs/OvervoltageFeedIn') == 1

	def _update_solarchargers_and_vecan(self, has_bms, bms_charge_voltage, max_charge_current, feedback_allowed, stop_on_mcc0):
		""" This function updates the solar chargers only. Parameters
		    related to the Multi are handled elsewhere. """

		# If the vebus service does not provide a charge voltage setpoint (so
		# no ESS/Hub-1/Hub-4), we use the max charge voltage provided by the
		# BMS (if any). This will probably prevent feedback, but that is
		# probably not allowed anyway.
		charge_voltage = None
		if self._multi.active:
			charge_voltage = self._multi.hub_voltage
		if charge_voltage is None and bms_charge_voltage is not None:
			charge_voltage = bms_charge_voltage
		if charge_voltage is not None:
			try:
				charge_voltage += self.solarvoltageoffset
			except (ValueError, TypeError):
				pass

		if charge_voltage is None and max_charge_current is None:
			return 0, 0, None

		voltage_written, current_written, network_mode = self._chargesystem.set_networked(
			has_bms, bms_charge_voltage, charge_voltage,
			max_charge_current, feedback_allowed, stop_on_mcc0)

		# Write the voltage to VE.Can. Also update the networkmode.
		if charge_voltage is not None:
			for service in self._vecan_services:
				try:
					# In case there is no path at all, the set_value below will
					# raise an DBusException which we will ignore cheerfully. If we
					# cannot set the NetworkMode there is no point in setting the
					# ChargeVoltage.
					self._dbusmonitor.set_value_async(service, '/Link/NetworkMode', network_mode)
					self._dbusmonitor.set_value_async(service, '/Link/ChargeVoltage', charge_voltage)
					voltage_written = 1
				except DBusException:
					pass

		return voltage_written, current_written, charge_voltage

	def _legacy_update_solarchargers(self):
		""" This is the old implementation we used before DVCC. It is kept
		    here so we can fall back to it where DVCC is not fully supported,
			and to avoid maintaining two copies of systemcalc. """

		max_charge_current = None
		for battery in BatteryService.instance.batteries:
			max_charge_current = safeadd(max_charge_current, battery.maxchargecurrent)

		# Workaround: copying the max charge current from BMS batteries to the solarcharger leads to problems:
		# excess PV power is not fed back to the grid any more, and loads on AC-out are not fed with PV power.
		# PV power is used for charging the batteries only.
		# So we removed this feature, until we have a complete solution for solar charger support. Until then
		# we set a 'high' max charge current to avoid 'BMS connection lost' alarms from the solarcharger.
		if max_charge_current is not None:
			max_charge_current = 1000

		vebus_path = self._multi.service if self._multi.active else None
		charge_voltage = None if vebus_path is None else \
			self._dbusmonitor.get_value(vebus_path, '/Hub/ChargeVoltage')

		if charge_voltage is None and max_charge_current is None:
			return (0, 0)

		# Network mode:
		# bit 0: Operated in network environment
		# bit 2: Remote Hub-1 control
		# bit 3: Remote BMS control
		network_mode = 1 | (0 if charge_voltage is None else 4) | (0 if max_charge_current is None else 8)
		voltage_written = 0
		current_written = 0
		for charger in self._chargesystem:
			try:
				# We use /Link/NetworkMode to detect Hub support in the solarcharger. Existence of this item
				# implies existence of the other /Link/* fields.
				if charger.networkmode is None:
					continue
				charger.networkmode = network_mode

				if charge_voltage is not None:
					charger.chargevoltage = charge_voltage
					voltage_written = 1

				if max_charge_current is not None:
					charger.maxchargecurrent = max_charge_current
					current_written = 1
			except DBusException:
				# If the charger for whatever reason doesn't have the /Link
				# path, ignore it. This is the legacy implementation and
				# better to keep it for the moment.
				pass

		# The below is different to the non-legacy case above, where the voltage
		# the com.victronenergy.vecan.* service instead.
		if charge_voltage is not None and self._chargesystem.has_vecan_chargers:
			# Charge voltage cannot by written directly to the CAN-bus solar chargers, we have to use
			# the com.victronenergy.vecan.* service instead.
			# Writing charge current to CAN-bus solar charger is not supported yet.
			for service in self._vecan_services:
				try:
					# Note: we don't check the value of charge_voltage_item because it may be invalid,
					# for example if the D-Bus path has not been written for more than 60 (?) seconds.
					# In case there is no path at all, the set_value below will raise an DBusException
					# which we will ignore cheerfully.
					self._dbusmonitor.set_value_async(service, '/Link/ChargeVoltage', charge_voltage)
					voltage_written = 1
				except DBusException:
					pass

		return (voltage_written, current_written)