Compare commits

...

7 Commits

Author SHA1 Message Date
Gavin
8bbf02438f
Merge f5bc880c62 into ae047758de 2026-01-04 08:47:01 -08:00
Necroneco
ae047758de
feat: add unit days to xiaomi.toothbrush.p001 (#1588)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2026-01-04 14:08:02 +08:00
GavinIves
f5bc880c62 fixbug 2025-12-28 15:24:34 +00:00
GavinIves
4c7b6d570d sync main 2025-12-28 15:10:02 +00:00
GavinIves
b95a2c18ee review 2025-12-28 14:29:55 +00:00
GavinIves
5dc6f400c2 fomatted code 2025-12-28 13:46:30 +00:00
GavinIves
006a445e3a Added command sending mode for lights to optimize the lighting effect 2025-12-28 13:35:02 +00:00
8 changed files with 428 additions and 42 deletions

View File

@ -47,11 +47,12 @@ Light entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any, Optional from typing import Any, Optional, List, Dict
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN, ATTR_COLOR_TEMP_KELVIN,
@ -86,7 +87,7 @@ async def async_setup_entry(
for miot_device in device_list: for miot_device in device_list:
for data in miot_device.entity_list.get('light', []): for data in miot_device.entity_list.get('light', []):
new_entities.append( new_entities.append(
Light(miot_device=miot_device, entity_data=data)) Light(miot_device=miot_device, entity_data=data, hass=hass))
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -106,10 +107,13 @@ class Light(MIoTServiceEntity, LightEntity):
_mode_map: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData self, miot_device: MIoTDevice, entity_data: MIoTEntityData,hass: HomeAssistant
) -> None: ) -> None:
"""Initialize the Light.""" """Initialize the Light."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self.hass = hass
self.miot_device = miot_device
self._command_send_mode_entity_id = None
self._attr_color_mode = None self._attr_color_mode = None
self._attr_supported_color_modes = set() self._attr_supported_color_modes = set()
self._attr_supported_features = LightEntityFeature(0) self._attr_supported_features = LightEntityFeature(0)
@ -252,42 +256,178 @@ class Light(MIoTServiceEntity, LightEntity):
""" """
# on # on
# Dirty logic for lumi.gateway.mgl03 indicator light # Dirty logic for lumi.gateway.mgl03 indicator light
if self._prop_on: # Determine whether the device sends the light-on properties in batches or one by one
value_on = True if self._prop_on.format_ == bool else 1 # Search entityid through unique_id to avoid the user modifying entityid and causing command_send_mode to not match
await self.set_property_async( # 获取开灯模式
prop=self._prop_on, value=value_on) if self._command_send_mode_entity_id is None:
# brightness entity_registry = async_get_entity_registry(self.hass)
device_id = list(
self.miot_device.device_info.get("identifiers"))[0][1]
self._command_send_mode_entity_id = entity_registry.async_get_entity_id(
"select", DOMAIN, f"select.light_{device_id}_command_send_mode")
if self._command_send_mode_entity_id is None:
_LOGGER.error(
"light command_send_mode not found, %s",
self.entity_id,
)
return
command_send_mode = self.hass.states.get(
self._command_send_mode_entity_id)
# 判断是先发送亮度还是先发送色温
send_brightness_first = False
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value( brightness_new = kwargs[ATTR_BRIGHTNESS]
self._brightness_scale, kwargs[ATTR_BRIGHTNESS]) brightness_old = self.brightness
await self.set_property_async( if brightness_old and brightness_new <= brightness_old:
prop=self._prop_brightness, value=brightness, send_brightness_first = True
write_ha_state=False)
# color-temperature # 开始发送开灯命令
if ATTR_COLOR_TEMP_KELVIN in kwargs: if command_send_mode and command_send_mode.state == "Send Together":
await self.set_property_async( set_properties_list: List[Dict[str, Any]] = []
prop=self._prop_color_temp, # mode
value=kwargs[ATTR_COLOR_TEMP_KELVIN], if ATTR_EFFECT in kwargs:
write_ha_state=False) set_properties_list.append({
self._attr_color_mode = ColorMode.COLOR_TEMP "prop":self._prop_mode,
# rgb color "value":self.get_map_key(
if ATTR_RGB_COLOR in kwargs: map_=self._mode_map,value=kwargs[ATTR_EFFECT]),
r = kwargs[ATTR_RGB_COLOR][0] })
g = kwargs[ATTR_RGB_COLOR][1] # brightness
b = kwargs[ATTR_RGB_COLOR][2] if send_brightness_first and ATTR_BRIGHTNESS in kwargs:
rgb = (r << 16) | (g << 8) | b brightness = brightness_to_value(
await self.set_property_async( self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
prop=self._prop_color, value=rgb, set_properties_list.append({
write_ha_state=False) "prop": self._prop_brightness,
self._attr_color_mode = ColorMode.RGB "value": brightness
# mode })
if ATTR_EFFECT in kwargs: # color-temperature
await self.set_property_async( if ATTR_COLOR_TEMP_KELVIN in kwargs:
prop=self._prop_mode, set_properties_list.append({
value=self.get_map_key( "prop": self._prop_color_temp,
map_=self._mode_map, value=kwargs[ATTR_EFFECT]), "value": kwargs[ATTR_COLOR_TEMP_KELVIN],
write_ha_state=False) })
self.async_write_ha_state() self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color
if ATTR_RGB_COLOR in kwargs:
r = kwargs[ATTR_RGB_COLOR][0]
g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b
set_properties_list.append({
"prop": self._prop_color,
"value": rgb
})
self._attr_color_mode = ColorMode.RGB
# brightness
if not send_brightness_first and ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
set_properties_list.append({
"prop": self._prop_brightness,
"value": brightness
})
if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1
set_properties_list.append({
"prop": self._prop_on,
"value": value_on
})
await self.set_properties_async(set_properties_list,write_ha_state=False)
self.async_write_ha_state()
elif command_send_mode and command_send_mode.state == "Send Turn On First":
set_properties_list: List[Dict[str, Any]] = []
if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1
set_properties_list.append({
"prop": self._prop_on,
"value": value_on
})
# mode
if ATTR_EFFECT in kwargs:
set_properties_list.append({
"prop":
self._prop_mode,
"value":
self.get_map_key(
map_=self._mode_map,value=kwargs[ATTR_EFFECT]),
})
# brightness
if send_brightness_first and ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
set_properties_list.append({
"prop": self._prop_brightness,
"value": brightness
})
# color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs:
set_properties_list.append({
"prop": self._prop_color_temp,
"value": kwargs[ATTR_COLOR_TEMP_KELVIN],
})
self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color
if ATTR_RGB_COLOR in kwargs:
r = kwargs[ATTR_RGB_COLOR][0]
g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b
set_properties_list.append({
"prop": self._prop_color,
"value": rgb
})
self._attr_color_mode = ColorMode.RGB
# brightness
if not send_brightness_first and ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
set_properties_list.append({
"prop": self._prop_brightness,
"value": brightness
})
await self.set_properties_async(set_properties_list,write_ha_state=False)
self.async_write_ha_state()
else:
if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1
await self.set_property_async(
prop=self._prop_on, value=value_on)
# brightness
if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
await self.set_property_async(
prop=self._prop_brightness, value=brightness,
write_ha_state=False)
# color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs:
await self.set_property_async(
prop=self._prop_color_temp,
value=kwargs[ATTR_COLOR_TEMP_KELVIN],
write_ha_state=False)
self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color
if ATTR_RGB_COLOR in kwargs:
r = kwargs[ATTR_RGB_COLOR][0]
g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b
await self.set_property_async(
prop=self._prop_color, value=rgb,
write_ha_state=False)
self._attr_color_mode = ColorMode.RGB
# mode
if ATTR_EFFECT in kwargs:
await self.set_property_async(
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=kwargs[ATTR_EFFECT]),
write_ha_state=False)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn the light off.""" """Turn the light off."""

View File

@ -46,7 +46,7 @@ off Xiaomi or its affiliates' products.
MIoT client instance. MIoT client instance.
""" """
from copy import deepcopy from copy import deepcopy
from typing import Any, Callable, Optional, final from typing import Any, Callable, Optional, final, Dict, List
import asyncio import asyncio
import json import json
import logging import logging
@ -710,6 +710,84 @@ class MIoTClient:
f'{self._i18n.translate("miot.client.device_exec_error")}, ' f'{self._i18n.translate("miot.client.device_exec_error")}, '
f'{self._i18n.translate("error.common.-10007")}') f'{self._i18n.translate("error.common.-10007")}')
async def set_props_async(
self, props_list: List[Dict[str, Any]],
) -> bool:
# props_list = [{'did': str, 'siid': int, 'piid': int, 'value': Any}......]
# 判断是不是只有一个did
did_set = {prop["did"] for prop in props_list}
if len(did_set) != 1:
raise MIoTClientError(f"more than one or no did once, {did_set}")
did = did_set.pop()
if did not in self._device_list_cache:
raise MIoTClientError(f"did not exist, {did}")
# Priority local control
if self._ctrl_mode == CtrlMode.AUTO:
# Gateway control
device_gw = self._device_list_gateway.get(did, None)
if (
device_gw and device_gw.get("online", False)
and device_gw.get("specv2_access", False) and "group_id" in device_gw
):
mips = self._mips_local.get(device_gw["group_id"], None)
if mips is None:
_LOGGER.error(
"no gateway route, %s, try control through cloud",
device_gw)
else:
result = await mips.set_props_async(
did=did,props_list=props_list)
_LOGGER.debug("gateway set props, %s -> %s", props_list, result)
rc = [(r or {}).get("code",
MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
for r in result]
if all(t in [0, 1] for t in rc):
return True
else:
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=next(x for x in rc if x not in (0, 1))))
# Lan control
device_lan = self._device_list_lan.get(did, None)
if device_lan and device_lan.get("online", False):
result = await self._miot_lan.set_props_async(
did=did, props_list=props_list)
_LOGGER.debug("lan set props, %s -> %s", props_list, result)
rc = [(r or {}).get("code",
MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
for r in result]
if all(t in [0, 1] for t in rc):
return True
else:
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=next(x for x in rc if x not in (0, 1))))
# Cloud control
device_cloud = self._device_list_cloud.get(did, None)
if device_cloud and device_cloud.get("online", False):
result = await self._http.set_props_async(params=props_list)
_LOGGER.debug(
"cloud set props, %s, result, %s",
props_list, result)
if result and len(result) == len(props_list):
rc = [(r or {}).get("code",
MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
for r in result]
if all(t in [0, 1] for t in rc):
return True
if any(t in [-704010000, -704042011] for t in rc):
# Device remove or offline
_LOGGER.error("device may be removed or offline, %s", did)
self._main_loop.create_task(
await
self.__refresh_cloud_device_with_dids_async(dids=[did]))
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=next(x for x in rc if x not in (0, 1))))
# Show error message
raise MIoTClientError(
f'{self._i18n.translate("miot.client.device_exec_error")}, '
f'{self._i18n.translate("error.common.-10007")}')
def request_refresh_prop( def request_refresh_prop(
self, did: str, siid: int, piid: int self, did: str, siid: int, piid: int
) -> None: ) -> None:

View File

@ -825,6 +825,22 @@ class MIoTHttpClient:
return res_obj['result'] return res_obj['result']
async def set_props_async(self, params: list) -> list:
"""
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
"""
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/miotspec/prop/set',
data={
'params': params
},
timeout=15
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']
async def action_async( async def action_async(
self, did: str, siid: int, aiid: int, in_list: list[dict] self, did: str, siid: int, aiid: int, in_list: list[dict]
) -> dict: ) -> dict:

View File

@ -47,7 +47,7 @@ MIoT device instance.
""" """
import asyncio import asyncio
from abc import abstractmethod from abc import abstractmethod
from typing import Any, Callable, Optional from typing import Any, Callable, Optional, Dict, List
import logging import logging
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -1082,6 +1082,49 @@ class MIoTServiceEntity(Entity):
self.async_write_ha_state() self.async_write_ha_state()
return True return True
async def set_properties_async(
self, set_properties_list: List[Dict[str, Any]],
update_value: bool = True, write_ha_state: bool = True,
) -> bool:
# set_properties_list = [{'prop': Optional[MIoTSpecProperty],
# 'value': Any}....]
for set_property in set_properties_list:
prop = set_property.get("prop")
value = set_property.get("value")
if not prop:
raise RuntimeError(
f'set property failed, property is None, '
f'{self.entity_id}, {self.name}')
value = prop.value_format(value)
value = prop.value_precision(value)
# 因为下面还有判断在这个循环里 所以这里要赋值回去
set_property["value"] = value
if prop not in self.entity_data.props:
raise RuntimeError(
f'set property failed, unknown property, '
f'{self.entity_id}, {self.name}, {prop.name}')
if not prop.writable:
raise RuntimeError(
f'set property failed, not writable, '
f'{self.entity_id}, {self.name}, {prop.name}')
try:
await self.miot_device.miot_client.set_props_async([{
"did": self.miot_device.did,
"siid": set_property["prop"].service.iid,
"piid": set_property["prop"].iid,
"value": set_property["value"],
} for set_property in set_properties_list])
except MIoTClientError as e:
raise RuntimeError(
f"{e}, {self.entity_id}, {self.name}, {'&'.join([set_property['prop'].name for set_property in set_properties_list])}") from e
if update_value:
for set_property in set_properties_list:
self._prop_value_map[
set_property["prop"]] = set_property["value"]
if write_ha_state:
self.async_write_ha_state()
return True
async def get_property_async(self, prop: MIoTSpecProperty) -> Any: async def get_property_async(self, prop: MIoTSpecProperty) -> Any:
if not prop: if not prop:
_LOGGER.error( _LOGGER.error(

View File

@ -58,7 +58,7 @@ import secrets
import socket import socket
import struct import struct
import threading import threading
from typing import Any, Callable, Coroutine, Optional, final from typing import Any, Callable, Coroutine, Optional, final, Dict, List
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -857,6 +857,29 @@ class MIoTLan:
return result_obj return result_obj
raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR) raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR)
@final
async def set_props_async(
self,did: str,props_list: List[Dict[str, Any]],
timeout_ms: int = 10000) -> dict:
# props_list = [{'did': did, 'siid': siid, 'piid': piid, 'value': value}......]
self.__assert_service_ready()
result_obj = await self.__call_api_async(
did=did, msg={
'method': 'set_properties',
'params': props_list,
}, timeout_ms=timeout_ms)
if result_obj:
if (
'result' in result_obj and
len(result_obj['result']) == len(props_list)
and result_obj['result'][0].get('did') == did
and all('code' in item for item in result_obj['result'])
):
return result_obj['result']
if 'code' in result_obj:
return result_obj
raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR)
@final @final
async def action_async( async def action_async(
self, did: str, siid: int, aiid: int, in_list: list, self, did: str, siid: int, aiid: int, in_list: list,

View File

@ -56,7 +56,7 @@ import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from typing import Any, Callable, Optional, final, Coroutine from typing import Any, Callable, Optional, final, Coroutine, Dict, List
from paho.mqtt.client import ( from paho.mqtt.client import (
MQTT_ERR_SUCCESS, MQTT_ERR_SUCCESS,
@ -1342,6 +1342,41 @@ class MipsLocalClient(_MipsClient):
'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value, 'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,
'message': 'Invalid result'} 'message': 'Invalid result'}
@final
async def set_props_async(
self, did: str, props_list: List[Dict[str, Any]],
timeout_ms: int = 10000
) -> dict:
# props_list= [{
# 'did': did,
# 'siid': siid,
# 'piid': piid,
# 'value': value
# }]
payload_obj: dict = {
"did": did,
"rpc": {
"id": self.__gen_mips_id,
"method": "set_properties",
"params": props_list,
}
}
result_obj = await self.__request_async(
topic="proxy/rpcReq",
payload=json.dumps(payload_obj),
timeout_ms=timeout_ms)
if result_obj:
if ("result" in result_obj and
len(result_obj["result"]) == len(props_list) and
result_obj["result"][0].get("did") == did and
all("code" in item for item in result_obj["result"])):
return result_obj["result"]
if "error" in result_obj:
return result_obj["error"]
return {
'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,
'message': 'Invalid result'}
@final @final
async def action_async( async def action_async(
self, did: str, siid: int, aiid: int, in_list: list, self, did: str, siid: int, aiid: int, in_list: list,

View File

@ -559,6 +559,9 @@ urn:miot-spec-v2:device:thermostat:0000A031:suittc-wk168:1:
description: '15' description: '15'
- value: 16 - value: 16
description: '16' description: '16'
urn:miot-spec-v2:device:toothbrush:0000A07E:xiaomi-p001:1:
prop.4.1041:
unit: days
urn:miot-spec-v2:device:water-purifier:0000A013:roswan-lte01:1:0000D05A: urn:miot-spec-v2:device:water-purifier:0000A013:roswan-lte01:1:0000D05A:
prop.4.1: prop.4.1:
unit: ppm unit: ppm

View File

@ -52,6 +52,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.restore_state import RestoreEntity
from .miot.const import DOMAIN from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTPropertyEntity from .miot.miot_device import MIoTDevice, MIoTPropertyEntity
@ -75,6 +77,17 @@ async def async_setup_entry(
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
# create select for light
new_light_select_entities = []
for miot_device in device_list:
# Add it to all devices with light entities, because some bathroom heaters and clothes drying racks also have lights.
# if "device:light" in miot_device.spec_instance.urn:
if miot_device.entity_list.get("light", []):
device_id = list(miot_device.device_info.get("identifiers"))[0][1]
new_light_select_entities.append(
LightCommandSendMode(hass=hass, device_id=device_id))
if new_light_select_entities:
async_add_entities(new_light_select_entities)
class Select(MIoTPropertyEntity, SelectEntity): class Select(MIoTPropertyEntity, SelectEntity):
"""Select entities for Xiaomi Home.""" """Select entities for Xiaomi Home."""
@ -94,3 +107,38 @@ class Select(MIoTPropertyEntity, SelectEntity):
def current_option(self) -> Optional[str]: def current_option(self) -> Optional[str]:
"""Return the current selected option.""" """Return the current selected option."""
return self.get_vlist_description(value=self._value) return self.get_vlist_description(value=self._value)
class LightCommandSendMode(SelectEntity, RestoreEntity):
"""To control whether to turn on the light, you need to send the light-on command first and
then send other color temperatures and brightness or send them all at the same time.
The default is to send one by one."""
def __init__(self, hass: HomeAssistant, device_id: str):
super().__init__()
self.hass = hass
self._device_id = device_id
self._attr_name = "Command Send Mode"
self.entity_id = f"select.light_{device_id}_command_send_mode"
self._attr_unique_id = self.entity_id
self._attr_options = [
"Send One by One", "Send Turn On First", "Send Together"
]
self._attr_device_info = {"identifiers": {(DOMAIN, device_id)}}
self._attr_current_option = self._attr_options[0]
self._attr_entity_category = EntityCategory.CONFIG
async def async_select_option(self, option: str):
if option in self._attr_options:
self._attr_current_option = option
self.async_write_ha_state()
async def async_added_to_hass(self):
await super().async_added_to_hass()
if (last_state := await self.async_get_last_state()
) and last_state.state in self._attr_options:
self._attr_current_option = last_state.state
@property
def current_option(self):
return self._attr_current_option