Refactor yeelight integration to use only flows (#51255)

* Refactor light.py to use only flows.py, eliminating transitions.py

* Reformat yeelight source code using black
This commit is contained in:
Daniel Rheinbay 2021-06-01 09:04:49 +02:00 committed by GitHub
parent 549b0b0727
commit 3c452f8c9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 46 additions and 152 deletions

View File

@ -164,16 +164,11 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
# Import manually configured devices # Import manually configured devices
for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items():
_LOGGER.debug("Importing configured %s", host) _LOGGER.debug("Importing configured %s", host)
entry_config = { entry_config = {CONF_HOST: host, **device_config}
CONF_HOST: host,
**device_config,
}
hass.async_create_task( hass.async_create_task(
hass.config_entries.flow.async_init( hass.config_entries.flow.async_init(
DOMAIN, DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config
context={"source": SOURCE_IMPORT}, )
data=entry_config,
),
) )
return True return True
@ -203,9 +198,7 @@ async def _async_initialize(
entry.async_on_unload( entry.async_on_unload(
async_dispatcher_connect( async_dispatcher_connect(
hass, hass, DEVICE_INITIALIZED.format(host), _async_load_platforms
DEVICE_INITIALIZED.format(host),
_async_load_platforms,
) )
) )
@ -224,10 +217,7 @@ def _async_populate_entry_options(hass: HomeAssistant, entry: ConfigEntry) -> No
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
entry, entry,
data={ data={CONF_HOST: entry.data.get(CONF_HOST), CONF_ID: entry.data.get(CONF_ID)},
CONF_HOST: entry.data.get(CONF_HOST),
CONF_ID: entry.data.get(CONF_ID),
},
options={ options={
CONF_NAME: entry.data.get(CONF_NAME, ""), CONF_NAME: entry.data.get(CONF_NAME, ""),
CONF_MODEL: entry.data.get(CONF_MODEL, ""), CONF_MODEL: entry.data.get(CONF_MODEL, ""),
@ -613,9 +603,7 @@ class YeelightEntity(Entity):
async def _async_get_device( async def _async_get_device(
hass: HomeAssistant, hass: HomeAssistant, host: str, entry: ConfigEntry
host: str,
entry: ConfigEntry,
) -> YeelightDevice: ) -> YeelightDevice:
# Get model from config and capabilities # Get model from config and capabilities
model = entry.options.get(CONF_MODEL) model = entry.options.get(CONF_MODEL)

View File

@ -83,10 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
self._set_confirm_only() self._set_confirm_only()
placeholders = { placeholders = {"model": self._discovered_model, "host": self._discovered_ip}
"model": self._discovered_model,
"host": self._discovered_ip,
}
self.context["title_placeholders"] = placeholders self.context["title_placeholders"] = placeholders
return self.async_show_form( return self.async_show_form(
step_id="discovery_confirm", description_placeholders=placeholders step_id="discovery_confirm", description_placeholders=placeholders
@ -105,8 +102,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else: else:
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=f"{model} {self.unique_id}", title=f"{model} {self.unique_id}", data=user_input
data=user_input,
) )
user_input = user_input or {} user_input = user_input or {}
@ -126,8 +122,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=_async_unique_name(capabilities), title=_async_unique_name(capabilities), data={CONF_ID: unique_id}
data={CONF_ID: unique_id},
) )
configured_devices = { configured_devices = {
@ -223,19 +218,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
{ {
vol.Optional(CONF_MODEL, default=options[CONF_MODEL]): str, vol.Optional(CONF_MODEL, default=options[CONF_MODEL]): str,
vol.Required( vol.Required(
CONF_TRANSITION, CONF_TRANSITION, default=options[CONF_TRANSITION]
default=options[CONF_TRANSITION],
): cv.positive_int, ): cv.positive_int,
vol.Required( vol.Required(
CONF_MODE_MUSIC, default=options[CONF_MODE_MUSIC] CONF_MODE_MUSIC, default=options[CONF_MODE_MUSIC]
): bool, ): bool,
vol.Required( vol.Required(
CONF_SAVE_ON_CHANGE, CONF_SAVE_ON_CHANGE, default=options[CONF_SAVE_ON_CHANGE]
default=options[CONF_SAVE_ON_CHANGE],
): bool, ): bool,
vol.Required( vol.Required(
CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH, default=options[CONF_NIGHTLIGHT_SWITCH]
default=options[CONF_NIGHTLIGHT_SWITCH],
): bool, ): bool,
} }
), ),

View File

@ -6,15 +6,7 @@ import logging
import voluptuous as vol import voluptuous as vol
import yeelight import yeelight
from yeelight import ( from yeelight import Bulb, BulbException, Flow, RGBTransition, SleepTransition, flows
Bulb,
BulbException,
Flow,
RGBTransition,
SleepTransition,
flows,
transitions as yee_transitions,
)
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
from homeassistant.components.light import ( from homeassistant.components.light import (
@ -180,9 +172,7 @@ SERVICE_SCHEMA_SET_MODE = {
vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode]) vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])
} }
SERVICE_SCHEMA_SET_MUSIC_MODE = { SERVICE_SCHEMA_SET_MUSIC_MODE = {vol.Required(ATTR_MODE_MUSIC): cv.boolean}
vol.Required(ATTR_MODE_MUSIC): cv.boolean,
}
SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA
@ -358,11 +348,7 @@ def _async_setup_services(hass: HomeAssistant):
transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]), transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]),
) )
await hass.async_add_executor_job( await hass.async_add_executor_job(
partial( partial(entity.set_scene, SceneClass.CF, flow)
entity.set_scene,
SceneClass.CF,
flow,
)
) )
async def _async_set_auto_delay_off_scene(entity, service_call): async def _async_set_auto_delay_off_scene(entity, service_call):
@ -378,24 +364,16 @@ def _async_setup_services(hass: HomeAssistant):
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_MODE, SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "set_mode"
SERVICE_SCHEMA_SET_MODE,
"set_mode",
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_START_FLOW, SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow
SERVICE_SCHEMA_START_FLOW,
_async_start_flow,
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_COLOR_SCENE, SERVICE_SET_COLOR_SCENE, SERVICE_SCHEMA_SET_COLOR_SCENE, _async_set_color_scene
SERVICE_SCHEMA_SET_COLOR_SCENE,
_async_set_color_scene,
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_HSV_SCENE, SERVICE_SET_HSV_SCENE, SERVICE_SCHEMA_SET_HSV_SCENE, _async_set_hsv_scene
SERVICE_SCHEMA_SET_HSV_SCENE,
_async_set_hsv_scene,
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_COLOR_TEMP_SCENE, SERVICE_SET_COLOR_TEMP_SCENE,
@ -413,9 +391,7 @@ def _async_setup_services(hass: HomeAssistant):
_async_set_auto_delay_off_scene, _async_set_auto_delay_off_scene,
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_MUSIC_MODE, SERVICE_SET_MUSIC_MODE, SERVICE_SCHEMA_SET_MUSIC_MODE, "set_music_mode"
SERVICE_SCHEMA_SET_MUSIC_MODE,
"set_music_mode",
) )
@ -707,11 +683,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
elif effect == EFFECT_FAST_RANDOM_LOOP: elif effect == EFFECT_FAST_RANDOM_LOOP:
flow = flows.random_loop(duration=250) flow = flows.random_loop(duration=250)
elif effect == EFFECT_WHATSAPP: elif effect == EFFECT_WHATSAPP:
flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) flow = flows.pulse(37, 211, 102, count=2)
elif effect == EFFECT_FACEBOOK: elif effect == EFFECT_FACEBOOK:
flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) flow = flows.pulse(59, 89, 152, count=2)
elif effect == EFFECT_TWITTER: elif effect == EFFECT_TWITTER:
flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) flow = flows.pulse(0, 172, 237, count=2)
else: else:
return return

View File

@ -74,9 +74,7 @@ YAML_CONFIGURATION = {
} }
} }
CONFIG_ENTRY_DATA = { CONFIG_ENTRY_DATA = {CONF_ID: ID}
CONF_ID: ID,
}
def _mocked_bulb(cannot_connect=False): def _mocked_bulb(cannot_connect=False):

View File

@ -56,17 +56,13 @@ async def test_discovery(hass: HomeAssistant):
assert not result["errors"] assert not result["errors"]
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
result["flow_id"],
{},
)
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["step_id"] == "pick_device" assert result2["step_id"] == "pick_device"
assert not result2["errors"] assert not result2["errors"]
with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch( with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch(
f"{MODULE}.async_setup_entry", f"{MODULE}.async_setup_entry", return_value=True
return_value=True,
) as mock_setup_entry: ) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_DEVICE: ID} result["flow_id"], {CONF_DEVICE: ID}
@ -87,10 +83,7 @@ async def test_discovery(hass: HomeAssistant):
assert not result["errors"] assert not result["errors"]
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
result["flow_id"],
{},
)
assert result2["type"] == "abort" assert result2["type"] == "abort"
assert result2["reason"] == "no_devices_found" assert result2["reason"] == "no_devices_found"
@ -102,10 +95,7 @@ async def test_discovery_no_device(hass: HomeAssistant):
) )
with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True): with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
result["flow_id"],
{},
)
assert result2["type"] == "abort" assert result2["type"] == "abort"
assert result2["reason"] == "no_devices_found" assert result2["reason"] == "no_devices_found"
@ -138,8 +128,7 @@ async def test_import(hass: HomeAssistant):
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.async_setup", return_value=True f"{MODULE}.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
f"{MODULE}.async_setup_entry", f"{MODULE}.async_setup_entry", return_value=True
return_value=True,
) as mock_setup_entry: ) as mock_setup_entry:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
@ -200,10 +189,7 @@ async def test_manual(hass: HomeAssistant):
mocked_bulb = _mocked_bulb() mocked_bulb = _mocked_bulb()
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.async_setup", return_value=True f"{MODULE}.async_setup", return_value=True
), patch( ), patch(f"{MODULE}.async_setup_entry", return_value=True):
f"{MODULE}.async_setup_entry",
return_value=True,
):
result4 = await hass.config_entries.flow.async_configure( result4 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: IP_ADDRESS} result["flow_id"], {CONF_HOST: IP_ADDRESS}
) )
@ -279,10 +265,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant):
type(mocked_bulb).get_capabilities = MagicMock(return_value=None) type(mocked_bulb).get_capabilities = MagicMock(return_value=None)
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.async_setup", return_value=True f"{MODULE}.async_setup", return_value=True
), patch( ), patch(f"{MODULE}.async_setup_entry", return_value=True):
f"{MODULE}.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: IP_ADDRESS} result["flow_id"], {CONF_HOST: IP_ADDRESS}
) )
@ -354,16 +337,13 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data):
mocked_bulb = _mocked_bulb() mocked_bulb = _mocked_bulb()
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN, context={"source": source}, data=data
context={"source": source},
data=data,
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None assert result["errors"] is None
with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch( with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch(
f"{MODULE}.async_setup_entry", f"{MODULE}.async_setup_entry", return_value=True
return_value=True,
) as mock_async_setup_entry: ) as mock_async_setup_entry:
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] == "create_entry" assert result2["type"] == "create_entry"
@ -393,9 +373,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data
type(mocked_bulb).get_capabilities = MagicMock(return_value=None) type(mocked_bulb).get_capabilities = MagicMock(return_value=None)
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN, context={"source": source}, data=data
context={"source": source},
data=data,
) )
assert result["type"] == RESULT_TYPE_ABORT assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect" assert result["reason"] == "cannot_connect"

View File

@ -45,12 +45,7 @@ from tests.common import MockConfigEntry
async def test_ip_changes_fallback_discovery(hass: HomeAssistant): async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
"""Test Yeelight ip changes and we fallback to discovery.""" """Test Yeelight ip changes and we fallback to discovery."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "5.5.5.5"}, unique_id=ID
data={
CONF_ID: ID,
CONF_HOST: "5.5.5.5",
},
unique_id=ID,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -60,12 +55,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
side_effect=[OSError, CAPABILITIES, CAPABILITIES] side_effect=[OSError, CAPABILITIES, CAPABILITIES]
) )
_discovered_devices = [ _discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}]
{
"capabilities": CAPABILITIES,
"ip": IP_ADDRESS,
}
]
with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.discover_bulbs", return_value=_discovered_devices f"{MODULE}.discover_bulbs", return_value=_discovered_devices
): ):
@ -92,12 +82,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant):
"""Test Yeelight ip changes and we fallback to discovery.""" """Test Yeelight ip changes and we fallback to discovery."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "5.5.5.5"})
domain=DOMAIN,
data={
CONF_HOST: "5.5.5.5",
},
)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb(True) mocked_bulb = _mocked_bulb(True)
@ -170,10 +155,7 @@ async def test_unique_ids_device(hass: HomeAssistant):
"""Test Yeelight unique IDs from yeelight device IDs.""" """Test Yeelight unique IDs from yeelight device IDs."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True},
**CONFIG_ENTRY_DATA,
CONF_NIGHTLIGHT_SWITCH: True,
},
unique_id=ID, unique_id=ID,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -197,11 +179,7 @@ async def test_unique_ids_device(hass: HomeAssistant):
async def test_unique_ids_entry(hass: HomeAssistant): async def test_unique_ids_entry(hass: HomeAssistant):
"""Test Yeelight unique IDs from entry IDs.""" """Test Yeelight unique IDs from entry IDs."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}
data={
**CONFIG_ENTRY_DATA,
CONF_NIGHTLIGHT_SWITCH: True,
},
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -231,12 +209,7 @@ async def test_unique_ids_entry(hass: HomeAssistant):
async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant):
"""Test Yeelight off while adding to ha, for example on HA start.""" """Test Yeelight off while adding to ha, for example on HA start."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_HOST: IP_ADDRESS}, unique_id=ID
data={
**CONFIG_ENTRY_DATA,
CONF_HOST: IP_ADDRESS,
},
unique_id=ID,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)

View File

@ -353,11 +353,7 @@ async def test_device_types(hass: HomeAssistant):
entity_id=ENTITY_LIGHT, entity_id=ENTITY_LIGHT,
): ):
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
data={
**CONFIG_ENTRY_DATA,
CONF_NIGHTLIGHT_SWITCH: False,
},
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -383,11 +379,7 @@ async def test_device_types(hass: HomeAssistant):
if nightlight_properties is None: if nightlight_properties is None:
return return
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}
data={
**CONFIG_ENTRY_DATA,
CONF_NIGHTLIGHT_SWITCH: True,
},
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await _async_setup(config_entry) await _async_setup(config_entry)
@ -577,16 +569,13 @@ async def test_effects(hass: HomeAssistant):
{YEELIGHT_SLEEP_TRANSACTION: [800]}, {YEELIGHT_SLEEP_TRANSACTION: [800]},
], ],
}, },
}, }
], ]
}, }
}, },
) )
config_entry = MockConfigEntry( config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
domain=DOMAIN,
data=CONFIG_ENTRY_DATA,
)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb() mocked_bulb = _mocked_bulb()