Example Lovelace YAML
```yaml
-# Example YAML for the dashboard shown above. Replace entity names and VIN with your vehicle info.
-title: Home
+# Example YAML for the dashboard shown above.
+title: Status
views:
- - badges: []
+ - icon: ''
+ title: Status
+ badges: []
cards:
+ - type: horizontal-stack
+ cards:
+ - entity: button.subaru_refresh
+ hold_action:
+ action: more-info
+ show_icon: true
+ show_name: false
+ show_state: false
+ tap_action:
+ action: call-service
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_refresh
+ type: button
+ icon_height: 48px
+ - entity: button.subaru_locate
+ hold_action:
+ action: more-info
+ show_icon: true
+ show_name: false
+ show_state: false
+ tap_action:
+ action: call-service
+ confirmation:
+ text: Poll Vehicle?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_locate
+ type: button
+ icon_height: 48px
+ title: Update Data
- cards:
- entity: sensor.subaru_odometer
name: Odometer
@@ -226,234 +297,106 @@ views:
type: vertical-stack
title: Tire Pressure
- type: vertical-stack
+ title: Remote Commands
cards:
- - type: horizontal-stack
- cards:
- - entity: ''
- hold_action:
+ - cards:
+ - type: button
+ tap_action:
action: more-info
- icon: mdi:refresh
- icon_height: 32px
- name: Refresh
- show_icon: true
- show_name: true
+ entity: lock.subaru_door_locks
show_state: false
+ show_name: false
+ icon_height: 48px
+ type: horizontal-stack
+ - cards:
+ - type: button
tap_action:
action: call-service
- service: subaru.fetch
- service_data:
- vin:
- type: button
- - entity: ''
- hold_action:
- action: more-info
- icon: mdi:car-connected
- icon_height: 32px
- name: Poll Vehicle
+ confirmation:
+ text: Flash lights?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id:
+ - button.subaru_lights_start
+ entity: button.subaru_lights_start
+ show_state: false
+ show_name: false
+ icon_height: 48px
+ - entity: button.subaru_lights_stop
+ icon_height: 48px
show_icon: true
- show_name: true
+ show_name: false
+ tap_action:
+ action: call-service
+ confirmation:
+ text: Stop lights?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_lights_stop
+ type: button
+ type: horizontal-stack
+ - cards:
+ - type: button
+ tap_action:
+ action: call-service
+ confirmation:
+ text: Sound horn?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_horn_start
+ entity: button.subaru_horn_start
show_state: false
+ show_name: false
+ icon_height: 48px
+ - entity: button.subaru_horn_stop
+ icon_height: 48px
+ show_icon: true
+ show_name: false
tap_action:
action: call-service
confirmation:
- text: Poll Vehicle?
- service: subaru.update
- service_data:
- vin:
+ text: Stop horn?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_horn_stop
type: button
- title: Update Data
- - type: vertical-stack
- title: Remote Commands
- cards:
- - cards:
- - icon: mdi:lock
- icon_height: 32px
- name: Lock
- show_icon: true
- show_name: true
- tap_action:
- action: call-service
- confirmation:
- text: Lock Doors?
- service: lock.lock
- service_data: {}
- target:
- entity_id: lock.subaru_door_lock
- type: button
- - entity: ''
- icon: mdi:lock-open-variant
- icon_height: 32px
- name: Unlock
- show_icon: true
- show_name: true
- show_state: false
- tap_action:
- action: call-service
- confirmation:
- text: Unlock Doors?
- service: lock.unlock
- service_data: {}
- target:
- entity_id: lock.subaru_door_lock
- type: button
- type: horizontal-stack
- - cards:
- - entity: ''
- hold_action:
- action: more-info
- icon: mdi:lightbulb-on
- icon_height: 32px
- name: Flash Lights
- show_icon: true
- show_name: true
- show_state: false
- tap_action:
- action: call-service
- confirmation:
- text: Flash lights?
- service: subaru.lights
- service_data:
- vin:
- type: button
- - entity: ''
- hold_action:
- action: more-info
- icon_height: 32px
- icon: mdi:lightbulb-off
- name: Stop Lights
- show_icon: true
- show_name: true
- show_state: false
- tap_action:
- action: call-service
- confirmation:
- text: Stop lights?
- service: subaru.lights_stop
- service_data:
- vin:
- type: button
- type: horizontal-stack
- - cards:
- - entity: ''
- hold_action:
- action: more-info
- icon: mdi:volume-high
- icon_height: 32px
- name: Sound Horn
- show_icon: true
- show_name: true
- show_state: false
- tap_action:
- action: call-service
- confirmation:
- text: Sound horn?
- service: subaru.horn
- service_data:
- vin:
- type: button
- - entity: ''
- hold_action:
- action: more-info
- icon_height: 32px
- icon: mdi:volume-off
- name: Stop Horn
- show_icon: true
- show_name: true
- show_state: false
- tap_action:
- action: call-service
- confirmation:
- text: Stop horn?
- service: subaru.horn_stop
- service_data:
- vin:
- type: button
- type: horizontal-stack
- - cards:
- - type: button
- hold_action:
- action: more-info
- icon: mdi:power
- icon_height: 32px
- name: Remote Start
- show_icon: true
- show_name: true
- tap_action:
- action: call-service
- confirmation:
- text: Remote Start?
- service: subaru.remote_start
- service_data:
- vin:
- - entity: ''
- hold_action:
- action: more-info
- icon: mdi:stop
- icon_height: 32px
- name: Remote Stop
- show_icon: true
- show_name: true
- tap_action:
- action: call-service
- confirmation:
- text: Remote Stop?
- service: subaru.remote_stop
- service_data:
- vin:
- type: button
- type: horizontal-stack
+ type: horizontal-stack
- cards:
- - cards:
- - entity: ''
- hold_action:
- action: more-info
- icon: mdi:battery-charging
- icon_height: 32px
- name: Begin Charging
- show_icon: true
- show_name: true
- show_state: false
- tap_action:
- action: call-service
- confirmation:
- text: Begin Charging?
- service: subaru.charge_start
- service_data:
- vin:
- type: button
- - entity: sensor.subaru_ev_battery_level
- name: EV Battery Level
- type: entity
- type: horizontal-stack
- - type: vertical-stack
- cards:
- - type: horizontal-stack
- cards:
- - type: entity
- entity: binary_sensor.subaru_ev_charge_port
- name: Plugged In
- - type: conditional
- conditions:
- - entity: sensor.subaru_ev_time_to_full_charge
- state_not: '1970-01-01T00:00:00'
- card:
- type: markdown
- content: >
- {% set time =
- (as_timestamp(states.sensor.subaru_ev_time_to_full_charge.state)
- - as_timestamp(now())) %}
-
- {% set hours = time // 3600 %}
-
- {% set minutes = time // 60 % 60 %}
-
- {% if int(hours) > 0 %}
-
- {{ int(hours) }} hours {% endif %}{{ int(minutes) }}
- minutes
- title: Time to Full Charge
- type: vertical-stack
- title: EV Functions
+ - entity: button.subaru_remote_start
+ icon_height: 48px
+ show_icon: true
+ show_name: false
+ tap_action:
+ action: call-service
+ confirmation:
+ text: Remote Start Car?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_remote_start
+ type: button
+ - type: button
+ tap_action:
+ action: call-service
+ confirmation:
+ text: Remote Stop Car?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_remote_stop
+ entity: button.subaru_remote_stop
+ show_state: false
+ show_name: false
+ icon_height: 48px
+ type: horizontal-stack
+ - type: entities
+ entities:
+ - entity: select.subaru_climate_preset
+ show_header_toggle: true
- type: vertical-stack
cards:
- detail: -2
@@ -467,7 +410,7 @@ views:
type: sensor
- type: entity
entity: binary_sensor.subaru_ignition
- name: Subaru Ignition
+ name: ' '
title: Miscellaneous Data
- card:
type: glance
@@ -507,14 +450,62 @@ views:
state_filter:
- 'on'
show_empty: false
+ - type: vertical-stack
+ title: EV Functions
+ cards:
+ - cards:
+ - entity: button.subaru_charge_ev
+ hold_action:
+ action: more-info
+ show_icon: true
+ show_name: false
+ show_state: false
+ tap_action:
+ action: call-service
+ confirmation:
+ text: Begin Charging?
+ service: button.press
+ service_data: {}
+ target:
+ entity_id: button.subaru_charge_ev
+ type: button
+ icon_height: 48px
+ - entity: sensor.subaru_ev_battery_level
+ name: EV Battery Level
+ type: entity
+ type: horizontal-stack
+ - type: vertical-stack
+ cards:
+ - type: horizontal-stack
+ cards:
+ - type: entity
+ entity: binary_sensor.subaru_ev_charge_port
+ name: Plugged In
+ - type: conditional
+ conditions:
+ - entity: sensor.subaru_ev_time_to_full_charge
+ state_not: '1969-12-31T19:00:00'
+ card:
+ type: markdown
+ content: >
+ {% set time =
+ (as_timestamp(states.sensor.subaru_ev_time_to_full_charge.state)
+ - as_timestamp(now())) %}
+
+ {% set hours = time // 3600 %}
+
+ {% set minutes = time // 60 % 60 %}
+
+ {% if int(hours) > 0 %}
+
+ {{ int(hours) }} hours {% endif %}{{ int(minutes) }}
+ minutes
+ title: Time to Full Charge
- type: map
entities:
- entity: device_tracker.subaru_location
hours_to_show: 0
- title: Subaru Location
- default_zoom: 14
- icon: ''
- title: Subaru
+ default_zoom: 3
```
\ No newline at end of file
diff --git a/custom_components/subaru/__init__.py b/custom_components/subaru/__init__.py
index 480d9ea..5d0e778 100644
--- a/custom_components/subaru/__init__.py
+++ b/custom_components/subaru/__init__.py
@@ -24,7 +24,8 @@
ENTRY_COORDINATOR,
ENTRY_VEHICLES,
FETCH_INTERVAL,
- REMOTE_SERVICE_FETCH,
+ REMOTE_CLIMATE_PRESET_NAME,
+ REMOTE_SERVICE_REMOTE_START,
SUPPORTED_PLATFORMS,
UPDATE_INTERVAL,
VEHICLE_API_GEN,
@@ -32,21 +33,20 @@
VEHICLE_HAS_REMOTE_SERVICE,
VEHICLE_HAS_REMOTE_START,
VEHICLE_HAS_SAFETY_SERVICE,
+ VEHICLE_LAST_FETCH,
VEHICLE_LAST_UPDATE,
VEHICLE_NAME,
VEHICLE_VIN,
)
from .remote_service import (
- SERVICES_THAT_NEED_FETCH,
async_call_remote_service,
get_supported_services,
+ refresh_subaru,
update_subaru,
)
_LOGGER = logging.getLogger(__name__)
-REMOTE_SERVICE_SCHEMA = vol.Schema({vol.Required(VEHICLE_VIN): cv.string})
-
async def async_setup(hass, base_config):
"""Do nothing since this integration does not support configuration.yml setup."""
@@ -57,7 +57,7 @@ async def async_setup(hass, base_config):
async def async_setup_entry(hass, entry):
"""Set up Subaru from a config entry."""
config = entry.data
- websession = aiohttp_client.async_get_clientsession(hass)
+ websession = aiohttp_client.async_create_clientsession(hass)
# Backwards compatibility for configs made before v0.3.0
country = config.get(CONF_COUNTRY)
@@ -119,18 +119,20 @@ async def async_update_data():
async def async_call_service(call):
"""Execute subaru service."""
vin = call.data[VEHICLE_VIN].upper()
+ arg = None
+ if call.service == REMOTE_SERVICE_REMOTE_START:
+ arg = call.data[REMOTE_CLIMATE_PRESET_NAME]
if vin in vehicles:
- if call.service != REMOTE_SERVICE_FETCH:
- await async_call_remote_service(
- hass,
- controller,
- call.service,
- vehicles[vin],
- entry.options.get(CONF_NOTIFICATION_OPTION),
- )
- if call.service in SERVICES_THAT_NEED_FETCH:
- await coordinator.async_refresh()
+ await async_call_remote_service(
+ hass,
+ controller,
+ call.service,
+ vehicles[vin],
+ arg,
+ entry.options.get(CONF_NOTIFICATION_OPTION),
+ )
+ await coordinator.async_refresh()
return
hass.components.persistent_notification.create(
@@ -141,9 +143,25 @@ async def async_call_service(call):
supported_services = get_supported_services(vehicles)
for service in supported_services:
- hass.services.async_register(
- DOMAIN, service, async_call_service, schema=REMOTE_SERVICE_SCHEMA
- )
+ if service == REMOTE_SERVICE_REMOTE_START:
+ hass.services.async_register(
+ DOMAIN,
+ service,
+ async_call_service,
+ schema=vol.Schema(
+ {
+ vol.Required(VEHICLE_VIN): cv.string,
+ vol.Required(REMOTE_CLIMATE_PRESET_NAME): cv.string,
+ }
+ ),
+ )
+ else:
+ hass.services.async_register(
+ DOMAIN,
+ service,
+ async_call_service,
+ schema=vol.Schema({vol.Required(VEHICLE_VIN): cv.string}),
+ )
return True
@@ -185,7 +203,7 @@ async def refresh_subaru_data(config_entry, vehicle_info, controller):
await update_subaru(vehicle, controller)
# Fetch data from Subaru servers
- await controller.fetch(vin, force=True)
+ await refresh_subaru(vehicle, controller)
# Update our local data that will go to entity states
received_data = await controller.get_data(vin)
@@ -206,5 +224,6 @@ def get_vehicle_info(controller, vin):
VEHICLE_HAS_REMOTE_SERVICE: controller.get_remote_status(vin),
VEHICLE_HAS_SAFETY_SERVICE: controller.get_safety_status(vin),
VEHICLE_LAST_UPDATE: 0,
+ VEHICLE_LAST_FETCH: 0,
}
return info
diff --git a/custom_components/subaru/binary_sensor.py b/custom_components/subaru/binary_sensor.py
index 6c307af..d398d49 100644
--- a/custom_components/subaru/binary_sensor.py
+++ b/custom_components/subaru/binary_sensor.py
@@ -103,6 +103,12 @@
SENSOR_CLASS: DEVICE_CLASS_WINDOW,
SENSOR_ON_VALUE: sc.WINDOW_OPEN,
},
+ {
+ SENSOR_TYPE: "Sunroof",
+ SENSOR_FIELD: sc.WINDOW_SUNROOF_STATUS,
+ SENSOR_CLASS: DEVICE_CLASS_WINDOW,
+ SENSOR_ON_VALUE: sc.WINDOW_OPEN,
+ },
]
# Binary Sensor data available to "Subaru Safety Plus" subscribers with PHEV vehicles
diff --git a/custom_components/subaru/button.py b/custom_components/subaru/button.py
new file mode 100644
index 0000000..b3a9e9e
--- /dev/null
+++ b/custom_components/subaru/button.py
@@ -0,0 +1,123 @@
+"""Support for Subaru buttons."""
+import logging
+
+from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonEntity
+
+from . import DOMAIN as SUBARU_DOMAIN
+from .const import (
+ CONF_NOTIFICATION_OPTION,
+ ENTRY_CONTROLLER,
+ ENTRY_COORDINATOR,
+ ENTRY_VEHICLES,
+ ICONS,
+ REMOTE_SERVICE_CHARGE_START,
+ REMOTE_SERVICE_FETCH,
+ REMOTE_SERVICE_HORN,
+ REMOTE_SERVICE_HORN_STOP,
+ REMOTE_SERVICE_LIGHTS,
+ REMOTE_SERVICE_LIGHTS_STOP,
+ REMOTE_SERVICE_REMOTE_START,
+ REMOTE_SERVICE_REMOTE_STOP,
+ REMOTE_SERVICE_UPDATE,
+ VEHICLE_CLIMATE_SELECTED_PRESET,
+ VEHICLE_HAS_EV,
+ VEHICLE_HAS_REMOTE_SERVICE,
+ VEHICLE_HAS_REMOTE_START,
+)
+from .entity import SubaruEntity
+from .remote_service import async_call_remote_service
+
+_LOGGER = logging.getLogger(__name__)
+
+BUTTON_TYPE = "type"
+BUTTON_SERVICE = "service"
+
+G1_REMOTE_BUTTONS = [
+ {BUTTON_TYPE: "Horn Start", BUTTON_SERVICE: REMOTE_SERVICE_HORN},
+ {BUTTON_TYPE: "Horn Stop", BUTTON_SERVICE: REMOTE_SERVICE_HORN_STOP},
+ {BUTTON_TYPE: "Lights Start", BUTTON_SERVICE: REMOTE_SERVICE_LIGHTS},
+ {BUTTON_TYPE: "Lights Stop", BUTTON_SERVICE: REMOTE_SERVICE_LIGHTS_STOP},
+ {BUTTON_TYPE: "Locate", BUTTON_SERVICE: REMOTE_SERVICE_UPDATE},
+ {BUTTON_TYPE: "Refresh", BUTTON_SERVICE: REMOTE_SERVICE_FETCH},
+]
+
+RES_REMOTE_BUTTONS = [
+ {BUTTON_TYPE: "Remote Start", BUTTON_SERVICE: REMOTE_SERVICE_REMOTE_START},
+ {BUTTON_TYPE: "Remote Stop", BUTTON_SERVICE: REMOTE_SERVICE_REMOTE_STOP},
+]
+
+EV_REMOTE_BUTTONS = [
+ {BUTTON_TYPE: "Charge EV", BUTTON_SERVICE: REMOTE_SERVICE_CHARGE_START}
+]
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Subaru button by config_entry."""
+ coordinator = hass.data[SUBARU_DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR]
+ vehicle_info = hass.data[SUBARU_DOMAIN][config_entry.entry_id][ENTRY_VEHICLES]
+ entities = []
+ for vin in vehicle_info:
+ entities.extend(
+ create_vehicle_buttons(vehicle_info[vin], coordinator, config_entry)
+ )
+ async_add_entities(entities, True)
+
+
+def create_vehicle_buttons(vehicle_info, coordinator, config_entry):
+ """Instantiate all available buttons for the vehicle."""
+ buttons_to_add = []
+ if vehicle_info[VEHICLE_HAS_REMOTE_SERVICE]:
+ buttons_to_add.extend(G1_REMOTE_BUTTONS)
+
+ if vehicle_info[VEHICLE_HAS_REMOTE_START] or vehicle_info[VEHICLE_HAS_EV]:
+ buttons_to_add.extend(RES_REMOTE_BUTTONS)
+
+ if vehicle_info[VEHICLE_HAS_EV]:
+ buttons_to_add.extend(EV_REMOTE_BUTTONS)
+
+ return [
+ SubaruButton(
+ vehicle_info, coordinator, config_entry, b[BUTTON_TYPE], b[BUTTON_SERVICE],
+ )
+ for b in buttons_to_add
+ ]
+
+
+class SubaruButton(SubaruEntity, ButtonEntity):
+ """Representation of a Subaru button."""
+
+ def __init__(self, vehicle_info, coordinator, config_entry, entity_type, service):
+ """Initialize the button for the vehicle."""
+ super().__init__(vehicle_info, coordinator)
+ self.entity_type = entity_type
+ self.hass_type = BUTTON_DOMAIN
+ self.config_entry = config_entry
+ self.service = service
+ self.arg = None
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ if not self.device_class:
+ return ICONS.get(self.entity_type)
+
+ async def async_press(self):
+ """Press the button."""
+ _LOGGER.debug("%s button pressed for %s", self.entity_type, self.car_name)
+ arg = None
+ if self.service == REMOTE_SERVICE_REMOTE_START:
+ arg = self.coordinator.data.get(self.vin).get(
+ VEHICLE_CLIMATE_SELECTED_PRESET
+ )
+ controller = self.hass.data[SUBARU_DOMAIN][self.config_entry.entry_id][
+ ENTRY_CONTROLLER
+ ]
+ await async_call_remote_service(
+ self.hass,
+ controller,
+ self.service,
+ self.vehicle_info,
+ arg,
+ self.config_entry.options.get(CONF_NOTIFICATION_OPTION),
+ )
+ await self.coordinator.async_refresh()
diff --git a/custom_components/subaru/const.py b/custom_components/subaru/const.py
index 23f6c22..9516d04 100644
--- a/custom_components/subaru/const.py
+++ b/custom_components/subaru/const.py
@@ -1,6 +1,8 @@
"""Constants for the Subaru integration."""
from enum import Enum
+import subarulink.const as sc
+
from homeassistant.const import Platform
DOMAIN = "subaru"
@@ -49,12 +51,17 @@ def get_by_value(cls, value):
VEHICLE_HAS_REMOTE_SERVICE = "has_remote"
VEHICLE_HAS_SAFETY_SERVICE = "has_safety"
VEHICLE_LAST_UPDATE = "last_update"
+VEHICLE_LAST_FETCH = "last_fetch"
VEHICLE_STATUS = "status"
+VEHICLE_CLIMATE = "climate"
+VEHICLE_CLIMATE_SELECTED_PRESET = "preset_name"
API_GEN_1 = "g1"
API_GEN_2 = "g2"
MANUFACTURER = "Subaru Corp."
+ATTR_DOOR = "door"
+
REMOTE_SERVICE_FETCH = "fetch"
REMOTE_SERVICE_UPDATE = "update"
REMOTE_SERVICE_LOCK = "lock"
@@ -66,12 +73,25 @@ def get_by_value(cls, value):
REMOTE_SERVICE_REMOTE_START = "remote_start"
REMOTE_SERVICE_REMOTE_STOP = "remote_stop"
REMOTE_SERVICE_CHARGE_START = "charge_start"
+REMOTE_CLIMATE_PRESET_NAME = "preset_name"
+
+SERVICE_UNLOCK_SPECIFIC_DOOR = "unlock_specific_door"
+UNLOCK_DOOR_ALL = "all"
+UNLOCK_DOOR_DRIVERS = "driver"
+UNLOCK_DOOR_TAILGATE = "tailgate"
+UNLOCK_VALID_DOORS = {
+ UNLOCK_DOOR_ALL: sc.ALL_DOORS,
+ UNLOCK_DOOR_DRIVERS: sc.DRIVERS_DOOR,
+ UNLOCK_DOOR_TAILGATE: sc.TAILGATE_DOOR,
+}
SUPPORTED_PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
Platform.LOCK,
Platform.SENSOR,
+ Platform.BUTTON,
+ Platform.SELECT,
]
ICONS = {
@@ -79,4 +99,14 @@ def get_by_value(cls, value):
"EV Range": "mdi:ev-station",
"Odometer": "mdi:road-variant",
"Range": "mdi:gas-station",
+ "Horn Start": "mdi:volume-high",
+ "Horn Stop": "mdi:volume-off",
+ "Lights Start": "mdi:lightbulb-on",
+ "Lights Stop": "mdi:lightbulb-off",
+ "Locate": "mdi:car-connected",
+ "Refresh": "mdi:refresh",
+ "Remote Start": "mdi:power",
+ "Remote Stop": "mdi:stop-circle-outline",
+ "Charge EV": "mdi:ev-station",
+ "Climate Preset": "mdi:thermometer-lines",
}
diff --git a/custom_components/subaru/lock.py b/custom_components/subaru/lock.py
index fc5f2ae..8b38498 100644
--- a/custom_components/subaru/lock.py
+++ b/custom_components/subaru/lock.py
@@ -1,15 +1,22 @@
"""Support for Subaru door locks."""
import logging
+import voluptuous as vol
+
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
from homeassistant.const import SERVICE_LOCK, SERVICE_UNLOCK
+from homeassistant.helpers import config_validation as cv, entity_platform
from . import DOMAIN as SUBARU_DOMAIN
from .const import (
+ ATTR_DOOR,
CONF_NOTIFICATION_OPTION,
ENTRY_CONTROLLER,
ENTRY_COORDINATOR,
ENTRY_VEHICLES,
+ SERVICE_UNLOCK_SPECIFIC_DOOR,
+ UNLOCK_DOOR_ALL,
+ UNLOCK_VALID_DOORS,
VEHICLE_HAS_REMOTE_SERVICE,
)
from .entity import SubaruEntity
@@ -29,6 +36,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(SubaruLock(vehicle, coordinator, controller, config_entry))
async_add_entities(entities, True)
+ platform = entity_platform.async_get_current_platform()
+
+ platform.async_register_entity_service(
+ SERVICE_UNLOCK_SPECIFIC_DOOR,
+ {vol.Required(ATTR_DOOR): cv.string},
+ "async_unlock_specific_door",
+ )
+
class SubaruLock(SubaruEntity, LockEntity):
"""
@@ -41,29 +56,44 @@ class SubaruLock(SubaruEntity, LockEntity):
def __init__(self, vehicle_info, coordinator, controller, config_entry):
"""Initialize the locks for the vehicle."""
super().__init__(vehicle_info, coordinator)
- self.entity_type = "Door Lock"
+ self.entity_type = "Door Locks"
self.hass_type = LOCK_DOMAIN
self.controller = controller
self.config_entry = config_entry
async def async_lock(self, **kwargs):
"""Send the lock command."""
- _LOGGER.debug("Locking doors for: %s", self.vin)
+ _LOGGER.debug("Locking doors for: %s", self.car_name)
await async_call_remote_service(
self.hass,
self.controller,
SERVICE_LOCK,
self.vehicle_info,
+ None,
self.config_entry.options.get(CONF_NOTIFICATION_OPTION),
)
async def async_unlock(self, **kwargs):
"""Send the unlock command."""
- _LOGGER.debug("Unlocking doors for: %s", self.vin)
+ _LOGGER.debug("Unlocking doors for: %s", self.car_name)
await async_call_remote_service(
self.hass,
self.controller,
SERVICE_UNLOCK,
self.vehicle_info,
+ UNLOCK_VALID_DOORS[UNLOCK_DOOR_ALL],
self.config_entry.options.get(CONF_NOTIFICATION_OPTION),
)
+
+ async def async_unlock_specific_door(self, door):
+ """Send the unlock command for a specified door."""
+ _LOGGER.debug("Unlocking %s door for: %s", self, self.car_name)
+ if door in UNLOCK_VALID_DOORS:
+ await async_call_remote_service(
+ self.hass,
+ self.controller,
+ SERVICE_UNLOCK,
+ self.vehicle_info,
+ UNLOCK_VALID_DOORS[door],
+ self.config_entry.options.get(CONF_NOTIFICATION_OPTION),
+ )
diff --git a/custom_components/subaru/manifest.json b/custom_components/subaru/manifest.json
index d2532af..beeee5e 100644
--- a/custom_components/subaru/manifest.json
+++ b/custom_components/subaru/manifest.json
@@ -4,8 +4,8 @@
"config_flow": true,
"documentation": "https://github.com/G-Two/homeassistant-subaru",
"issue_tracker": "https://github.com/G-Two/homeassistant-subaru/issues",
- "requirements": ["subarulink==0.3.16"],
+ "requirements": ["subarulink==0.4.0"],
"codeowners": ["@G-Two"],
- "version": "0.5.2",
+ "version": "0.6.0",
"iot_class": "cloud_polling"
}
diff --git a/custom_components/subaru/remote_service.py b/custom_components/subaru/remote_service.py
index 77862ce..621cdd7 100644
--- a/custom_components/subaru/remote_service.py
+++ b/custom_components/subaru/remote_service.py
@@ -8,6 +8,7 @@
from .const import (
DOMAIN,
+ FETCH_INTERVAL,
REMOTE_SERVICE_CHARGE_START,
REMOTE_SERVICE_FETCH,
REMOTE_SERVICE_HORN,
@@ -16,12 +17,14 @@
REMOTE_SERVICE_LIGHTS_STOP,
REMOTE_SERVICE_REMOTE_START,
REMOTE_SERVICE_REMOTE_STOP,
+ REMOTE_SERVICE_UNLOCK,
REMOTE_SERVICE_UPDATE,
UPDATE_INTERVAL,
VEHICLE_HAS_EV,
VEHICLE_HAS_REMOTE_SERVICE,
VEHICLE_HAS_REMOTE_START,
VEHICLE_HAS_SAFETY_SERVICE,
+ VEHICLE_LAST_FETCH,
VEHICLE_LAST_UPDATE,
VEHICLE_NAME,
VEHICLE_VIN,
@@ -39,8 +42,10 @@
]
-async def async_call_remote_service(hass, controller, cmd, vehicle_info, notify_option):
- """Execute subarulink remote command with start/end notification."""
+async def async_call_remote_service(
+ hass, controller, cmd, vehicle_info, arg, notify_option
+):
+ """Execute subarulink remote command with optional start/end notification."""
car_name = vehicle_info[VEHICLE_NAME]
vin = vehicle_info[VEHICLE_VIN]
notify = NotificationOptions.get_by_value(notify_option)
@@ -58,8 +63,18 @@ async def async_call_remote_service(hass, controller, cmd, vehicle_info, notify_
success = await update_subaru(
vehicle_info, controller, override_interval=True
)
+ elif cmd in [REMOTE_SERVICE_REMOTE_START, REMOTE_SERVICE_UNLOCK]:
+ success = await getattr(controller, cmd)(vin, arg)
+ elif cmd == REMOTE_SERVICE_FETCH:
+ pass
else:
success = await getattr(controller, cmd)(vin)
+
+ if cmd in SERVICES_THAT_NEED_FETCH:
+ success = await refresh_subaru(
+ vehicle_info, controller, override_interval=True
+ )
+
except SubaruException as err:
err_msg = err.message
@@ -114,3 +129,17 @@ async def update_subaru(vehicle, controller, override_interval=False):
vehicle[VEHICLE_LAST_UPDATE] = cur_time
return success
+
+
+async def refresh_subaru(vehicle, controller, override_interval=False):
+ """Refresh data from Subaru servers."""
+ cur_time = time.time()
+ last_fetch = vehicle[VEHICLE_LAST_FETCH]
+ vin = vehicle[VEHICLE_VIN]
+ success = None
+
+ if (cur_time - last_fetch) > FETCH_INTERVAL or override_interval:
+ success = await controller.fetch(vin, force=True)
+ vehicle[VEHICLE_LAST_FETCH] = cur_time
+
+ return success
diff --git a/custom_components/subaru/select.py b/custom_components/subaru/select.py
new file mode 100644
index 0000000..b4f460d
--- /dev/null
+++ b/custom_components/subaru/select.py
@@ -0,0 +1,78 @@
+"""Support for Subaru selectors."""
+import logging
+
+from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity
+from homeassistant.helpers.restore_state import RestoreEntity
+
+from . import DOMAIN as SUBARU_DOMAIN
+from .const import (
+ ENTRY_COORDINATOR,
+ ENTRY_VEHICLES,
+ VEHICLE_CLIMATE,
+ VEHICLE_CLIMATE_SELECTED_PRESET,
+ VEHICLE_HAS_EV,
+ VEHICLE_HAS_REMOTE_START,
+)
+from .entity import SubaruEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Subaru selectors by config_entry."""
+ coordinator = hass.data[SUBARU_DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR]
+ vehicle_info = hass.data[SUBARU_DOMAIN][config_entry.entry_id][ENTRY_VEHICLES]
+ climate_select = []
+ for vin in vehicle_info:
+ if (
+ vehicle_info[vin][VEHICLE_HAS_REMOTE_START]
+ or vehicle_info[vin][VEHICLE_HAS_EV]
+ ):
+ climate_select.append(
+ SubaruClimateSelect(
+ "Climate Preset", vehicle_info[vin], coordinator, config_entry
+ )
+ )
+ async_add_entities(climate_select, True)
+
+
+class SubaruClimateSelect(SubaruEntity, SelectEntity, RestoreEntity):
+ """Representation of a Subaru climate preset selector entity."""
+
+ def __init__(self, type, vehicle_info, coordinator, config_entry):
+ """Initialize the selector for the vehicle."""
+ super().__init__(vehicle_info, coordinator)
+ self.entity_type = type
+ self.hass_type = SELECT_DOMAIN
+ self.config_entry = config_entry
+ self._attr_current_option = None
+
+ @property
+ def options(self):
+ """Return a set of selectable options."""
+ vehicle_data = self.coordinator.data.get(self.vin)
+ if vehicle_data:
+ preset_data = vehicle_data.get(VEHICLE_CLIMATE)
+ if isinstance(preset_data, list):
+ return [preset["name"] for preset in preset_data]
+
+ async def async_added_to_hass(self):
+ """Restore previous state of this selector."""
+ await super().async_added_to_hass()
+ state = await self.async_get_last_state()
+ if state and state.state in self.options:
+ self._attr_current_option = state.state
+ self.coordinator.data.get(self.vin)[
+ VEHICLE_CLIMATE_SELECTED_PRESET
+ ] = state.state
+ self.async_write_ha_state()
+
+ async def async_select_option(self, option):
+ """Change the selected option."""
+ _LOGGER.debug("Selecting %s climate preset for %s", option, self.car_name)
+ if option in self.options:
+ self._attr_current_option = option
+ self.coordinator.data.get(self.vin)[
+ VEHICLE_CLIMATE_SELECTED_PRESET
+ ] = option
+ self.async_write_ha_state()
diff --git a/custom_components/subaru/services.yaml b/custom_components/subaru/services.yaml
index 41436a9..5e0c2b3 100644
--- a/custom_components/subaru/services.yaml
+++ b/custom_components/subaru/services.yaml
@@ -2,7 +2,7 @@
lights:
description: >
Flash the lights of the vehicle. The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
@@ -12,7 +12,7 @@ lights:
lights_stop:
description: >
Stop flashing the lights of the vehicle. The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
@@ -22,7 +22,7 @@ lights_stop:
horn:
description: >
Sound the horn of the vehicle. The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
@@ -32,7 +32,7 @@ horn:
horn_stop:
description: >
Stop sounding the horn of the vehicle. The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
@@ -42,17 +42,21 @@ horn_stop:
remote_start:
description: >
Start the engine and climate control of the vehicle. Uses the climate control settings saved. The vehicle is identified via
- the vin (see below).
+ the vin (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: JF2ABCDE6L0000001
+ preset_name:
+ description: >
+ The name of the climate control preset desired
+ example: Full Heat
remote_stop:
description: >
Stop the engine and climate control of the vehicle. The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
@@ -62,7 +66,7 @@ remote_stop:
charge_start:
description: >
Starts EV charging. This cannot be stopped remotely. The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
@@ -72,7 +76,7 @@ charge_start:
update:
description: >
Sends request to vehicle to update data. The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
@@ -82,9 +86,29 @@ update:
fetch:
description: >
Refreshes data (does not request update from vehicle). The vehicle is identified via the vin
- (see below).
+ (see below). This service is deprecated and will be removed - Use button service instead.
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: JF2ABCDE6L0000001
+
+unlock_specific_door:
+ name: Unlock Specific Door
+ description: Unlocks only the door specified
+ target:
+ entity:
+ domain: lock
+ integration: subaru
+ fields:
+ door:
+ name: Door
+ description: "One of the following: 'all', 'driver', 'tailgate'"
+ example: driver
+ required: true
+ selector:
+ select:
+ options:
+ - "all"
+ - "driver"
+ - "tailgate"
diff --git a/hacs.json b/hacs.json
index 89b3317..35cd576 100644
--- a/hacs.json
+++ b/hacs.json
@@ -2,6 +2,6 @@
"name": "Subaru (HACS)",
"content_in_root": false,
"render_readme": true,
- "homeassistant": "0.118.0",
+ "homeassistant": "2021.12",
"iot_class": "Cloud Polling"
}
diff --git a/requirements.test.txt b/requirements.test.txt
index b4bdd77..2739bfe 100644
--- a/requirements.test.txt
+++ b/requirements.test.txt
@@ -2,4 +2,4 @@ flake8
pytest
pytest-cov
pytest-homeassistant-custom-component
-subarulink==0.3.16
+subarulink==0.4.0
diff --git a/tests/api_responses.py b/tests/api_responses.py
index a179f52..01be0c5 100644
--- a/tests/api_responses.py
+++ b/tests/api_responses.py
@@ -11,11 +11,14 @@
API_GEN_1,
API_GEN_2,
VEHICLE_API_GEN,
+ VEHICLE_CLIMATE,
+ VEHICLE_CLIMATE_SELECTED_PRESET,
VEHICLE_HAS_EV,
VEHICLE_HAS_REMOTE_SERVICE,
VEHICLE_HAS_REMOTE_START,
VEHICLE_HAS_SAFETY_SERVICE,
VEHICLE_NAME,
+ VEHICLE_STATUS,
VEHICLE_VIN,
)
@@ -54,7 +57,95 @@
}
VEHICLE_STATUS_EV = {
- "status": {
+ VEHICLE_CLIMATE_SELECTED_PRESET: None,
+ VEHICLE_CLIMATE: [
+ {
+ "name": "Auto",
+ "runTimeMinutes": "10",
+ "climateZoneFrontTemp": "74",
+ "climateZoneFrontAirMode": "AUTO",
+ "climateZoneFrontAirVolume": "AUTO",
+ "outerAirCirculation": "auto",
+ "heatedRearWindowActive": "false",
+ "airConditionOn": "false",
+ "heatedSeatFrontLeft": "off",
+ "heatedSeatFrontRight": "off",
+ "startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION",
+ "canEdit": "true",
+ "disabled": "false",
+ "vehicleType": "gas",
+ "presetType": "subaruPreset",
+ },
+ {
+ "name": "Full Cool",
+ "runTimeMinutes": "10",
+ "climateZoneFrontTemp": "60",
+ "climateZoneFrontAirMode": "feet_face_balanced",
+ "climateZoneFrontAirVolume": "7",
+ "airConditionOn": "true",
+ "heatedSeatFrontLeft": "OFF",
+ "heatedSeatFrontRight": "OFF",
+ "heatedRearWindowActive": "false",
+ "outerAirCirculation": "outsideAir",
+ "startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION",
+ "canEdit": "true",
+ "disabled": "true",
+ "vehicleType": "gas",
+ "presetType": "subaruPreset",
+ },
+ {
+ "name": "Full Heat",
+ "runTimeMinutes": "10",
+ "climateZoneFrontTemp": "85",
+ "climateZoneFrontAirMode": "feet_window",
+ "climateZoneFrontAirVolume": "7",
+ "airConditionOn": "false",
+ "heatedSeatFrontLeft": "high_heat",
+ "heatedSeatFrontRight": "high_heat",
+ "heatedRearWindowActive": "true",
+ "outerAirCirculation": "outsideAir",
+ "startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION",
+ "canEdit": "true",
+ "disabled": "true",
+ "vehicleType": "gas",
+ "presetType": "subaruPreset",
+ },
+ {
+ "name": "Full Cool",
+ "runTimeMinutes": "10",
+ "climateZoneFrontTemp": "60",
+ "climateZoneFrontAirMode": "feet_face_balanced",
+ "climateZoneFrontAirVolume": "7",
+ "airConditionOn": "true",
+ "heatedSeatFrontLeft": "OFF",
+ "heatedSeatFrontRight": "OFF",
+ "heatedRearWindowActive": "false",
+ "outerAirCirculation": "outsideAir",
+ "startConfiguration": "START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION",
+ "canEdit": "true",
+ "disabled": "true",
+ "vehicleType": "phev",
+ "presetType": "subaruPreset",
+ },
+ {
+ "name": "Full Heat",
+ "runTimeMinutes": "10",
+ "climateZoneFrontTemp": "85",
+ "climateZoneFrontAirMode": "feet_window",
+ "climateZoneFrontAirVolume": "7",
+ "airConditionOn": "false",
+ "heatedSeatFrontLeft": "high_heat",
+ "heatedSeatFrontRight": "high_heat",
+ "heatedRearWindowActive": "true",
+ "outerAirCirculation": "outsideAir",
+ "startConfiguration": "START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION",
+ "canEdit": "true",
+ "disabled": "true",
+ "vehicleType": "phev",
+ "presetType": "subaruPreset",
+ },
+ ],
+ VEHICLE_STATUS: {
"AVG_FUEL_CONSUMPTION": 2.3,
"BATTERY_VOLTAGE": "12.0",
"DISTANCE_TO_EMPTY_FUEL": 707,
@@ -126,7 +217,7 @@
"heading": 170,
"latitude": 40.0,
"longitude": -100.0,
- }
+ },
}
VEHICLE_STATUS_EV_INVALID_ITEMS = {
@@ -376,6 +467,7 @@
"WINDOW_FRONT_RIGHT_STATUS": "unavailable",
"WINDOW_REAR_LEFT_STATUS": "unavailable",
"WINDOW_REAR_RIGHT_STATUS": "unavailable",
+ "WINDOW_SUNROOF_STATUS": "unavailable",
"VEHICLE_STATE_TYPE": "unavailable",
"DOOR_FRONT_LEFT_POSITION": "unavailable",
"DOOR_FRONT_RIGHT_POSITION": "unavailable",
@@ -392,6 +484,7 @@
"WINDOW_FRONT_RIGHT_STATUS": None,
"WINDOW_REAR_LEFT_STATUS": None,
"WINDOW_REAR_RIGHT_STATUS": None,
+ "WINDOW_SUNROOF_STATUS": None,
"VEHICLE_STATE_TYPE": "off",
"DOOR_FRONT_LEFT_POSITION": "off",
"DOOR_FRONT_RIGHT_POSITION": "off",
diff --git a/tests/conftest.py b/tests/conftest.py
index 4c11fae..be94eb0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,6 +6,7 @@
from pytest_homeassistant_custom_component.common import (
MockConfigEntry,
async_fire_time_changed,
+ mock_restore_cache,
)
from subarulink.const import COUNTRY_USA
@@ -27,6 +28,7 @@
from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME
+from homeassistant.core import State
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -47,6 +49,8 @@
MOCK_API_GET_DATA = f"{MOCK_API}get_data"
MOCK_API_UPDATE = f"{MOCK_API}update"
MOCK_API_FETCH = f"{MOCK_API}fetch"
+MOCK_API_REMOTE_START = f"{MOCK_API}remote_start"
+MOCK_API_LIGHTS = f"{MOCK_API}lights"
TEST_USERNAME = "user@email.com"
TEST_PASSWORD = "password"
@@ -95,8 +99,14 @@ async def setup_subaru_integration(
vehicle_status=None,
connect_effect=None,
fetch_effect=None,
+ saved_cache=None,
):
"""Create Subaru entry."""
+ if saved_cache:
+ mock_restore_cache(
+ hass, (State("select.test_vehicle_2_climate_preset", "Full Heat"),),
+ )
+
assert await async_setup_component(hass, HA_DOMAIN, {})
assert await async_setup_component(hass, DOMAIN, {})
@@ -150,3 +160,20 @@ async def ev_entry(hass, enable_custom_integrations):
assert hass.config_entries.async_get_entry(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
return entry
+
+
+@pytest.fixture
+async def ev_entry_with_saved_climate(hass, enable_custom_integrations):
+ """Create Subaru EV entity but with saved climate preset."""
+ entry = await setup_subaru_integration(
+ hass,
+ vehicle_list=[TEST_VIN_2_EV],
+ vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
+ vehicle_status=VEHICLE_STATUS_EV,
+ saved_cache=True,
+ )
+ assert DOMAIN in hass.config_entries.async_domains()
+ assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+ assert hass.config_entries.async_get_entry(entry.entry_id)
+ assert entry.state is ConfigEntryState.LOADED
+ return entry
diff --git a/tests/test_button.py b/tests/test_button.py
new file mode 100644
index 0000000..9f05548
--- /dev/null
+++ b/tests/test_button.py
@@ -0,0 +1,59 @@
+"""Test Subaru buttons."""
+
+from unittest.mock import patch
+
+from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
+from homeassistant.const import ATTR_ENTITY_ID
+
+from .conftest import MOCK_API_FETCH, MOCK_API_LIGHTS, MOCK_API_REMOTE_START
+
+REMOTE_START_BUTTON = "button.test_vehicle_2_remote_start"
+REMOTE_LIGHTS_BUTTON = "button.test_vehicle_2_lights_start"
+REMOTE_REFRESH_BUTTON = "button.test_vehicle_2_refresh"
+
+
+async def test_device_exists(hass, ev_entry):
+ """Test subaru button entity exists."""
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = entity_registry.async_get(REMOTE_START_BUTTON)
+ assert entry
+
+
+async def test_button_with_fetch(hass, ev_entry):
+ """Test subaru button function."""
+ with patch(MOCK_API_REMOTE_START) as mock_remote_start, patch(
+ MOCK_API_FETCH
+ ) as mock_fetch:
+ await hass.services.async_call(
+ BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: REMOTE_START_BUTTON}, blocking=True
+ )
+ await hass.async_block_till_done()
+ mock_remote_start.assert_called_once()
+ mock_fetch.assert_called_once()
+
+
+async def test_button_without_fetch(hass, ev_entry):
+ """Test subaru button function."""
+ with patch(MOCK_API_LIGHTS) as mock_lights, patch(MOCK_API_FETCH) as mock_fetch:
+ await hass.services.async_call(
+ BUTTON_DOMAIN,
+ "press",
+ {ATTR_ENTITY_ID: REMOTE_LIGHTS_BUTTON},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_lights.assert_called_once()
+ mock_fetch.assert_not_called()
+
+
+async def test_button_fetch(hass, ev_entry):
+ """Test subaru button function."""
+ with patch(MOCK_API_FETCH) as mock_fetch:
+ await hass.services.async_call(
+ BUTTON_DOMAIN,
+ "press",
+ {ATTR_ENTITY_ID: REMOTE_REFRESH_BUTTON},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_fetch.assert_called_once()
diff --git a/tests/test_lock.py b/tests/test_lock.py
index 6d4e936..eedfbcb 100644
--- a/tests/test_lock.py
+++ b/tests/test_lock.py
@@ -2,6 +2,13 @@
from unittest.mock import patch
+from custom_components.subaru.const import (
+ ATTR_DOOR,
+ DOMAIN as SUBARU_DOMAIN,
+ SERVICE_UNLOCK_SPECIFIC_DOOR,
+ UNLOCK_DOOR_DRIVERS,
+ UNLOCK_VALID_DOORS,
+)
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK
@@ -9,7 +16,7 @@
MOCK_API_LOCK = f"{MOCK_API}lock"
MOCK_API_UNLOCK = f"{MOCK_API}unlock"
-DEVICE_ID = "lock.test_vehicle_2_door_lock"
+DEVICE_ID = "lock.test_vehicle_2_door_locks"
async def test_device_exists(hass, ev_entry):
@@ -37,3 +44,16 @@ async def test_unlock(hass, ev_entry):
)
await hass.async_block_till_done()
mock_unlock.assert_called_once()
+
+
+async def test_unlock_specific_door(hass, ev_entry):
+ """Test subaru unlock specific door function."""
+ with patch(MOCK_API_UNLOCK) as mock_unlock:
+ await hass.services.async_call(
+ SUBARU_DOMAIN,
+ SERVICE_UNLOCK_SPECIFIC_DOOR,
+ {ATTR_ENTITY_ID: DEVICE_ID, ATTR_DOOR: UNLOCK_DOOR_DRIVERS},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_unlock.assert_called_once()
diff --git a/tests/test_select.py b/tests/test_select.py
new file mode 100644
index 0000000..83ed03c
--- /dev/null
+++ b/tests/test_select.py
@@ -0,0 +1,26 @@
+"""Test Subaru select."""
+
+from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
+from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION
+
+DEVICE_ID = "select.test_vehicle_2_climate_preset"
+
+
+async def test_device_exists(hass, ev_entry):
+ """Test subaru select entity exists."""
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = entity_registry.async_get(DEVICE_ID)
+ assert entry
+ await hass.async_block_till_done()
+
+
+async def test_select(hass, ev_entry_with_saved_climate):
+ """Test subaru select function."""
+ await hass.services.async_call(
+ SELECT_DOMAIN,
+ SERVICE_SELECT_OPTION,
+ {ATTR_ENTITY_ID: DEVICE_ID, ATTR_OPTION: "Full Heat"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(DEVICE_ID).state == "Full Heat"
diff --git a/tests/test_services.py b/tests/test_services.py
index 9100f77..9f63117 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -6,8 +6,10 @@
from custom_components.subaru.const import (
DOMAIN,
+ REMOTE_CLIMATE_PRESET_NAME,
REMOTE_SERVICE_FETCH,
REMOTE_SERVICE_HORN,
+ REMOTE_SERVICE_REMOTE_START,
REMOTE_SERVICE_UPDATE,
VEHICLE_VIN,
)
@@ -18,6 +20,7 @@
from tests.conftest import MOCK_API, MOCK_API_FETCH, MOCK_API_GET_DATA, MOCK_API_UPDATE
MOCK_API_HORN = f"{MOCK_API}horn"
+MOCK_API_REMOTE_START = f"{MOCK_API}remote_start"
async def test_remote_service_horn(hass, ev_entry):
@@ -30,6 +33,22 @@ async def test_remote_service_horn(hass, ev_entry):
mock_horn.assert_called_once()
+async def test_remote_service_start(hass, ev_entry):
+ """Test remote service horn."""
+ with patch(MOCK_API_REMOTE_START) as mock_remote_start, patch(
+ MOCK_API_FETCH
+ ) as mock_fetch:
+ await hass.services.async_call(
+ DOMAIN,
+ REMOTE_SERVICE_REMOTE_START,
+ {VEHICLE_VIN: TEST_VIN_2_EV, REMOTE_CLIMATE_PRESET_NAME: "Full Cool"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_remote_start.assert_called_once()
+ mock_fetch.assert_called_once()
+
+
async def test_remote_service_fetch(hass, ev_entry):
"""Test remote service fetch."""
with patch(MOCK_API_GET_DATA, return_value=VEHICLE_STATUS_EV), patch(