Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

333 custom spotprice sensor field #335

Merged
merged 9 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions custom_components/peaqev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, conf: ConfigEntry) -> bool:
'cautionhours',
'_startpeaks',
'nonhours',
'custom_price_sensor'
]

RELOAD_CHANGES = ['fuse_type', 'gain_loss', 'price_aware', 'name', 'powersensorincludescar']
Expand Down Expand Up @@ -90,9 +91,7 @@ async def async_set_options(conf) -> HubOptions:
if options.charger.chargertype == ChargerType.NoCharger.value:
options.powersensor_includes_car = True
else:
options.powersensor_includes_car = conf.options.get('powersensorincludescar', conf.data.get(
'powersensorincludescar', False
))
options.powersensor_includes_car = await async_get_existing_param(conf, 'powersensorincludescar', False)

options.startpeaks = conf.options.get('startpeaks', conf.data.get('startpeaks'))
options.use_peak_history = conf.options.get('use_peak_history', conf.data.get('use_peak_history', False))
Expand All @@ -105,6 +104,8 @@ async def async_set_options(conf) -> HubOptions:
options.nonhours = await async_get_existing_param(conf, 'priceaware_nonhours', [])
else:
options.nonhours = await async_get_existing_param(conf, 'nonhours', [])
custom_sensor = await async_get_existing_param(conf, 'custom_price_sensor', None)
options.price.custom_sensor = custom_sensor if custom_sensor and len(custom_sensor) > 2 else None
options.price.min_price = await async_get_existing_param(
conf, 'min_priceaware_threshold_price', 0
)
Expand All @@ -115,7 +116,7 @@ async def async_set_options(conf) -> HubOptions:
conf, 'dynamic_top_price', False
)
options.price.cautionhour_type = await async_get_existing_param(
conf, 'cautionhour_type', 'intermediate'
conf, 'cautionhour_type', 'suave'
)
options.max_charge = conf.options.get('max_charge', 0)
options.fuse_type = await async_get_existing_param(conf, 'mains', '')
Expand Down
37 changes: 27 additions & 10 deletions custom_components/peaqev/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async def async_step_sensor(self, user_input=None):
errors['base'] = 'invalid_powersensor'
if not errors:
self.data.update(user_input)
return await self.async_step_charger()
return await self.async_step_charger()

return self.async_show_form(
step_id='sensor', data_schema=SENSOR_SCHEMA, errors=errors, last_step=False
Expand Down Expand Up @@ -116,10 +116,15 @@ async def async_step_outletdetails(self, user_input=None):
async def async_step_priceaware(self, user_input=None):
errors = {}
if user_input is not None:
self.data.update(user_input)
if self.data['priceaware'] is False:
return await self.async_step_hours()
return await self.async_step_priceaware_hours()
try:
await ConfigFlowValidation.validate_price_sensor(self.hass, user_input['custom_price_sensor'])
except ValueError:
errors['base'] = 'invalid_pricesensor'
if not errors:
self.data.update(user_input)
if self.data['priceaware'] is False:
return await self.async_step_hours()
return await self.async_step_priceaware_hours()

return self.async_show_form(
step_id='priceaware',
Expand Down Expand Up @@ -198,13 +203,24 @@ async def _get_existing_param(self, parameter: str, default_val: any):

async def async_step_init(self, user_input=None):
"""Priceaware"""
errors = {}
if user_input is not None:
self.options.update(user_input)
if self.options['priceaware'] is False:
return await self.async_step_hours()
return await self.async_step_sensor()
if len(user_input['custom_price_sensor']) > 2:
try:
await ConfigFlowValidation.validate_price_sensor(self.hass, user_input['custom_price_sensor'])
except ValueError:
errors['base'] = 'invalid_pricesensor'
else:
_LOGGER.info('Nulling Custom price sensor')
user_input['custom_price_sensor'] = ''
if not errors:
self.options.update(user_input)
if self.options['priceaware'] is False:
return await self.async_step_hours()
return await self.async_step_sensor()

_priceaware = await self._get_existing_param('priceaware', False)
_custompricesensor = await self._get_existing_param('custom_price_sensor', '')
_topprice = await self._get_existing_param('absolute_top_price', 0)
_minprice = await self._get_existing_param('min_priceaware_threshold_price', 0)
_hourtype = await self._get_existing_param('cautionhour_type', CautionHourType.INTERMEDIATE.value)
Expand All @@ -214,9 +230,11 @@ async def async_step_init(self, user_input=None):
return self.async_show_form(
step_id='init',
last_step=False,
errors=errors,
data_schema=vol.Schema(
{
vol.Optional('priceaware', default=_priceaware): cv.boolean,
vol.Optional('custom_price_sensor', default=_custompricesensor): cv.string,
vol.Optional('dynamic_top_price', default=_dynamic_top_price): cv.boolean,
vol.Optional('absolute_top_price', default=_topprice): cv.positive_float,
vol.Optional('min_priceaware_threshold_price', default=_minprice): cv.positive_float,
Expand All @@ -233,7 +251,6 @@ async def async_step_sensor(self, user_input=None):
errors = {}
if user_input is not None:
try:
self.info = await ConfigFlowValidation.validate_input_first(user_input)
await ConfigFlowValidation.validate_power_sensor(self.hass, user_input['name'])
except ValueError:
errors['base'] = 'invalid_powersensor'
Expand Down
63 changes: 32 additions & 31 deletions custom_components/peaqev/configflow/config_flow_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,86 +11,87 @@
TYPE_SCHEMA = vol.Schema(
{
vol.Optional(
"peaqevtype",
default="",
'peaqevtype',
default='',
): vol.In(INSTALLATIONTYPES)
}
)

SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional("powersensorincludescar", default=False): cv.boolean,
vol.Optional('powersensorincludescar', default=False): cv.boolean,
}
)

CHARGER_SCHEMA = vol.Schema(
{
vol.Optional(
"chargertype",
default="",
'chargertype',
default='',
): vol.In(CHARGERTYPES),
vol.Optional(
"locale",
default="",
'locale',
default='',
): vol.In(LOCALES),
}
)

CHARGER_DETAILS_SCHEMA = vol.Schema(
{
vol.Optional("chargerid"): cv.string,
vol.Optional('chargerid'): cv.string,
}
)

OUTLET_DETAILS_SCHEMA = vol.Schema(
{
vol.Optional("outletswitch"): cv.string,
vol.Optional("outletpowermeter"): cv.string,
vol.Optional('outletswitch'): cv.string,
vol.Optional('outletpowermeter'): cv.string,
}
)

HOURS_SCHEMA = vol.Schema(
{
vol.Optional("nonhours"): cv.multi_select(list(range(0, 24))),
vol.Optional("cautionhours"): cv.multi_select(list(range(0, 24))),
vol.Optional('nonhours'): cv.multi_select(list(range(0, 24))),
vol.Optional('cautionhours'): cv.multi_select(list(range(0, 24))),
}
)

PRICEAWARE_HOURS_SCHEMA = vol.Schema(
{
vol.Optional("priceaware_nonhours"): cv.multi_select(list(range(0, 24))),
vol.Optional('priceaware_nonhours'): cv.multi_select(list(range(0, 24))),
}
)

PRICEAWARE_SCHEMA = vol.Schema(
{
vol.Optional("priceaware", default=False): cv.boolean,
vol.Optional("absolute_top_price"): cv.positive_float,
vol.Optional("dynamic_top_price", default=False): cv.boolean,
vol.Optional("min_priceaware_threshold_price"): cv.positive_float,
vol.Optional('priceaware', default=False): cv.boolean,
vol.Optional('custom_price_sensor'): cv.string,
vol.Optional('absolute_top_price'): cv.positive_float,
vol.Optional('dynamic_top_price', default=False): cv.boolean,
vol.Optional('min_priceaware_threshold_price'): cv.positive_float,
vol.Optional(
"cautionhour_type",
'cautionhour_type',
default=CautionHourType.INTERMEDIATE.value,
): vol.In(CAUTIONHOURTYPE_NAMES),
}
)

MONTHS_SCHEMA = vol.Schema(
{
vol.Optional("jan", default=1.5): cv.positive_float,
vol.Optional("feb", default=1.5): cv.positive_float,
vol.Optional("mar", default=1.5): cv.positive_float,
vol.Optional("apr", default=1.5): cv.positive_float,
vol.Optional("may", default=1.5): cv.positive_float,
vol.Optional("jun", default=1.5): cv.positive_float,
vol.Optional("jul", default=1.5): cv.positive_float,
vol.Optional("aug", default=1.5): cv.positive_float,
vol.Optional("sep", default=1.5): cv.positive_float,
vol.Optional("oct", default=1.5): cv.positive_float,
vol.Optional("nov", default=1.5): cv.positive_float,
vol.Optional("dec", default=1.5): cv.positive_float,
vol.Optional("use_peak_history", default=False): cv.boolean,
vol.Optional('jan', default=1.5): cv.positive_float,
vol.Optional('feb', default=1.5): cv.positive_float,
vol.Optional('mar', default=1.5): cv.positive_float,
vol.Optional('apr', default=1.5): cv.positive_float,
vol.Optional('may', default=1.5): cv.positive_float,
vol.Optional('jun', default=1.5): cv.positive_float,
vol.Optional('jul', default=1.5): cv.positive_float,
vol.Optional('aug', default=1.5): cv.positive_float,
vol.Optional('sep', default=1.5): cv.positive_float,
vol.Optional('oct', default=1.5): cv.positive_float,
vol.Optional('nov', default=1.5): cv.positive_float,
vol.Optional('dec', default=1.5): cv.positive_float,
vol.Optional('use_peak_history', default=False): cv.boolean,
}
)

Expand Down
46 changes: 38 additions & 8 deletions custom_components/peaqev/configflow/config_flow_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,67 @@
class ConfigFlowValidation:
@staticmethod
async def validate_power_sensor(hass: HomeAssistant, powersensor: str):
_powersensor = f"sensor.{powersensor}" if not powersensor.startswith("sensor.") else powersensor
_powersensor = f'sensor.{powersensor}' if not powersensor.startswith('sensor.') else powersensor
val_state = hass.states.get(_powersensor)
if val_state is None:
_LOGGER.error(f"Could not validate chosen powersensor {_powersensor}. It returned None as state.")
_LOGGER.error(f'Could not validate chosen powersensor {_powersensor}. It returned None as state.')
raise FaultyPowerSensor
elif not isinstance(float(val_state.state), float):
_LOGGER.error(
f"Could not validate chosen powersensor {_powersensor}. The value of its state is not a number. {val_state.state}"
f'Could not validate chosen powersensor {_powersensor}. The value of its state is not a number. {val_state.state}'
)
raise FaultyPowerSensor

@staticmethod
async def validate_price_sensor(hass: HomeAssistant, pricesensor:str):
_pricesensor = f'sensor.{pricesensor}' if not pricesensor.startswith('sensor.') else pricesensor
val_state = hass.states.get(_pricesensor)
if val_state is None:
raise FaultyPowerSensor('It returned None as state.')

if not isinstance(float(val_state.state), float):
raise FaultyPriceSensor(f'Value of state is not a number. {val_state.state}')

try:
ConfigFlowValidation.validate_price_sensor_attributes(val_state.attributes)
except Exception as e:
raise FaultyPriceSensor(f'Could not validate chosen pricing sensor. {e}')

@staticmethod
def validate_price_sensor_attributes(attributes: dict):
if 'today' not in attributes:
raise ValueError('Today values is missing')
if 'tomorrow_valid' not in attributes:
raise ValueError('Tomorrow valid is missing.')
if 'currency' not in attributes:
raise ValueError('Currency is missing.')

@staticmethod
async def validate_input_first(data: dict) -> dict[str, Any]:
if len(data["name"]) < 3:
if len(data['name']) < 3:
raise ValueError
if not data["name"].startswith("sensor."):
data["name"] = f"sensor.{data['name']}"
return {"title": data["name"]}
if not data['name'].startswith('sensor.'):
data['name'] = f"sensor.{data['name']}"
return {'title': data['name']}

@staticmethod
async def validate_input_first_chargerid(data: dict) -> dict[str, Any]:
"""Validate the chargerId"""
# if len(data["chargerid"]) < 1:
# raise ValueError

return {"title": data["name"]}
return {'title': data['name']}


class FaultyPowerSensor(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

class FaultyPriceSensor(exceptions.HomeAssistantError):
def __init__(self, message=None):
base_message = 'Could not validate chosen pricing sensor.'
if message:
base_message += ' ' + message
super().__init__(base_message)

class InvalidHost(exceptions.HomeAssistantError):
"""Error to indicate there is an invalid hostname."""
2 changes: 1 addition & 1 deletion custom_components/peaqev/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/elden1337/hass-peaq/issues",
"requirements": [
"peaqevcore==19.8.1"
"peaqevcore==19.9.0"
],
"version": "3.4.5"
}
4 changes: 2 additions & 2 deletions custom_components/peaqev/peaqservice/hub/hub_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def async_setup(hub: HomeAssistantHub) -> HomeAssistantHub:
hub.prediction = Prediction(hub) # threshold
hub.servicecalls = ServiceCalls(hub) # top level
hub.states = await StateChangesFactory.async_create(hub) # top level
hub.spotprice = SpotPriceFactory.create(hub=hub, observer=hub.observer, system=PeaqSystem.PeaqEv, test=False,is_active=hub.options.price.price_aware)
hub.spotprice = SpotPriceFactory.create(hub=hub, observer=hub.observer, system=PeaqSystem.PeaqEv, test=False,is_active=hub.options.price.price_aware, custom_sensor=hub.options.price.custom_sensor)
hub.power = await PowerToolsFactory.async_create(hub)
hub.events = HubEvents(hub, hub.state_machine)
return hub
return hub
15 changes: 8 additions & 7 deletions custom_components/peaqev/peaqservice/hub/models/hub_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ class Price(OptionsComparer):
price_aware: bool = False
min_price: float = 0.0
top_price: float = 0.0
cautionhour_type: str = ""
cautionhour_type: str = ''
dynamic_top_price: bool = False
custom_sensor: str = None


@dataclass
class Charger(OptionsComparer):
chargertype: str = ""
chargerid: str = ""
powerswitch: str = ""
powermeter: str = ""
chargertype: str = ''
chargerid: str = ''
powerswitch: str = ''
powermeter: str = ''


@dataclass
Expand All @@ -41,7 +42,7 @@ class HubOptions:
_startpeaks: dict = field(default_factory=dict)
cautionhours: List = field(default_factory=lambda: [])
nonhours: List = field(default_factory=lambda: [])
fuse_type: str = ""
fuse_type: str = ''
gainloss: bool = False
max_charge: int = 0
use_peak_history: bool = False
Expand All @@ -62,7 +63,7 @@ def compare(self, other) -> list:
for key, value in self.__dict__.items():
if key not in other.__dict__.keys():
diff.append(key)
elif key in ["charger", "price"]:
elif key in ['charger', 'price']:
diff.extend(value.compare(other=other.__dict__[key]))
elif value != other.__dict__[key]:
diff.append(key)
Expand Down
2 changes: 2 additions & 0 deletions custom_components/peaqev/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"init": {
"data": {
"priceaware": "[%key:common::config_flow::data::priceaware%]",
"custom_price_sensor": "[%key:common::config_flow::data::custom_price_sensor%]",
"absolute_top_price": "[%key:common::config_flow::data::absolute_top_price%]",
"dynamic_top_price": "[%key:common::config_flow::data::dynamic_top_price%]",
"min_priceaware_threshold_price": "[%key:common::config_flow::data::min_priceaware_threshold_price%]",
Expand Down Expand Up @@ -86,6 +87,7 @@
"priceaware": {
"data": {
"priceaware": "[%key:common::config_flow::data::priceaware%]",
"custom_price_sensor": "[%key:common::config_flow::data::custom_price_sensor%]",
"absolute_top_price": "[%key:common::config_flow::data::absolute_top_price%]",
"dynamic_top_price": "[%key:common::config_flow::data::dynamic_top_price%]",
"min_priceaware_threshold_price": "[%key:common::config_flow::data::min_priceaware_threshold_price%]",
Expand Down
Loading