async def test_get_automation_trace(hass, hass_ws_client): """Test tracing an automation.""" id = 1 def next_id(): nonlocal id id += 1 return id sun_config = { "id": "sun", "trigger": {"platform": "event", "event_type": "test_event"}, "action": {"service": "test.automation"}, } moon_config = { "id": "moon", "trigger": [ {"platform": "event", "event_type": "test_event2"}, {"platform": "event", "event_type": "test_event3"}, ], "condition": { "condition": "template", "value_template": "{{ trigger.event.event_type=='test_event2' }}", }, "action": {"event": "another_event"}, } assert await async_setup_component( hass, "automation", { "automation": [ sun_config, moon_config, ] }, ) with patch.object(config, "SECTIONS", ["automation"]): await async_setup_component(hass, "config", {}) client = await hass_ws_client() contexts = {} # Trigger "sun" automation context = Context() hass.bus.async_fire("test_event", context=context) await hass.async_block_till_done() # List traces await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] run_id = _find_run_id(response["result"], "sun") # Get trace await client.send_json( { "id": next_id(), "type": "automation/trace/get", "automation_id": "sun", "run_id": run_id, } ) response = await client.receive_json() assert response["success"] trace = response["result"] assert trace["context"]["parent_id"] == context.id assert len(trace["action_trace"]) == 1 assert len(trace["action_trace"]["action/0"]) == 1 assert trace["action_trace"]["action/0"][0]["error"] assert "result" not in trace["action_trace"]["action/0"][0] assert trace["condition_trace"] == {} assert trace["config"] == sun_config assert trace["context"] assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" assert trace["trigger"] == "event 'test_event'" assert trace["unique_id"] == "sun" assert trace["variables"] contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], "automation_id": trace["automation_id"], } # Trigger "moon" automation, with passing condition hass.bus.async_fire("test_event2") await hass.async_block_till_done() # List traces await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] run_id = _find_run_id(response["result"], "moon") # Get trace await client.send_json( { "id": next_id(), "type": "automation/trace/get", "automation_id": "moon", "run_id": run_id, } ) response = await client.receive_json() assert response["success"] trace = response["result"] assert len(trace["action_trace"]) == 1 assert len(trace["action_trace"]["action/0"]) == 1 assert "error" not in trace["action_trace"]["action/0"][0] assert "result" not in trace["action_trace"]["action/0"][0] assert len(trace["condition_trace"]) == 1 assert len(trace["condition_trace"]["condition/0"]) == 1 assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True} assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" assert trace["trigger"] == "event 'test_event2'" assert trace["unique_id"] == "moon" assert trace["variables"] contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], "automation_id": trace["automation_id"], } # Trigger "moon" automation, with failing condition hass.bus.async_fire("test_event3") await hass.async_block_till_done() # List traces await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] run_id = _find_run_id(response["result"], "moon") # Get trace await client.send_json( { "id": next_id(), "type": "automation/trace/get", "automation_id": "moon", "run_id": run_id, } ) response = await client.receive_json() assert response["success"] trace = response["result"] assert len(trace["action_trace"]) == 0 assert len(trace["condition_trace"]) == 1 assert len(trace["condition_trace"]["condition/0"]) == 1 assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": False} assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" assert trace["trigger"] == "event 'test_event3'" assert trace["unique_id"] == "moon" assert trace["variables"] contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], "automation_id": trace["automation_id"], } # Trigger "moon" automation, with passing condition hass.bus.async_fire("test_event2") await hass.async_block_till_done() # List traces await client.send_json({"id": next_id(), "type": "automation/trace/list"}) response = await client.receive_json() assert response["success"] run_id = _find_run_id(response["result"], "moon") # Get trace await client.send_json( { "id": next_id(), "type": "automation/trace/get", "automation_id": "moon", "run_id": run_id, } ) response = await client.receive_json() assert response["success"] trace = response["result"] assert len(trace["action_trace"]) == 1 assert len(trace["action_trace"]["action/0"]) == 1 assert "error" not in trace["action_trace"]["action/0"][0] assert "result" not in trace["action_trace"]["action/0"][0] assert len(trace["condition_trace"]) == 1 assert len(trace["condition_trace"]["condition/0"]) == 1 assert trace["condition_trace"]["condition/0"][0]["result"] == {"result": True} assert trace["config"] == moon_config assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" assert trace["trigger"] == "event 'test_event2'" assert trace["unique_id"] == "moon" assert trace["variables"] contexts[trace["context"]["id"]] = { "run_id": trace["run_id"], "automation_id": trace["automation_id"], } # Check contexts await client.send_json({"id": next_id(), "type": "automation/trace/contexts"}) response = await client.receive_json() assert response["success"] assert response["result"] == contexts
async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_only_user): """Test the reload config service.""" assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { "service": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } }, ) assert hass.states.get("automation.hello") is not None assert hass.states.get("automation.bye") is None listeners = hass.bus.async_listeners() assert listeners.get("test_event") == 1 assert listeners.get("test_event2") is None hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data.get("event") == "test_event" test_reload_event = async_capture_events(hass, EVENT_AUTOMATION_RELOADED) with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ automation.DOMAIN: { "alias": "bye", "trigger": {"platform": "event", "event_type": "test_event2"}, "action": { "service": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } }, ): with pytest.raises(Unauthorized): await hass.services.async_call( automation.DOMAIN, SERVICE_RELOAD, context=Context(user_id=hass_read_only_user.id), blocking=True, ) await hass.services.async_call( automation.DOMAIN, SERVICE_RELOAD, context=Context(user_id=hass_admin_user.id), blocking=True, ) # De-flake ?! await hass.async_block_till_done() assert len(test_reload_event) == 1 assert hass.states.get("automation.hello") is None assert hass.states.get("automation.bye") is not None listeners = hass.bus.async_listeners() assert listeners.get("test_event") is None assert listeners.get("test_event2") == 1 hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 hass.bus.async_fire("test_event2") await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data.get("event") == "test_event2"
async def test_if_fires_on_zone_enter(hass, calls): """Test for firing on zone enter.""" context = Context() hass.states.async_set( "geo_location.entity", "hello", { "latitude": 32.881011, "longitude": -117.234758, "source": "test_source" }, ) await hass.async_block_till_done() assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: { "trigger": { "platform": "geo_location", "source": "test_source", "zone": "zone.test", "event": "enter", }, "action": { "service": "test.automation", "data_template": { "some": "{{ trigger.%s }}" % "}} - {{ trigger.".join(( "platform", "entity_id", "from_state.state", "to_state.state", "zone.name", )) }, }, } }, ) hass.states.async_set( "geo_location.entity", "hello", { "latitude": 32.880586, "longitude": -117.237564 }, context=context, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert (calls[0].data["some"] == "geo_location - geo_location.entity - hello - hello - test") # Set out of zone again so we can trigger call hass.states.async_set( "geo_location.entity", "hello", { "latitude": 32.881011, "longitude": -117.234758 }, ) await hass.async_block_till_done() await hass.services.async_call( automation.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True, ) hass.states.async_set( "geo_location.entity", "hello", { "latitude": 32.880586, "longitude": -117.237564 }, ) await hass.async_block_till_done() assert len(calls) == 1
def _state_empty_context(hass, entity_id): # We don't restore context unless we need it by joining the # events table on the event_id for state_changed events state = hass.states.get(entity_id) state.context = Context(id=None) return state
def context(self, msg: dict[str, Any]) -> Context: """Return a context.""" return Context(user_id=self.user.id)
async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) ent_reg = er.async_get(hass) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "test_1": {"initial": 50, "min": 0, "max": 51}, "test_3": {"initial": 10, "min": 0, "max": 15}, } }, ) assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_number.test_1") state_2 = hass.states.get("input_number.test_2") state_3 = hass.states.get("input_number.test_3") assert state_1 is not None assert state_2 is None assert state_3 is not None assert float(state_1.state) == 50 assert float(state_3.state) == 10 assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: { "test_1": {"initial": 40, "min": 0, "max": 51}, "test_2": {"initial": 20, "min": 10, "max": 30}, } }, ): with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_read_only_user.id), ) await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_admin_user.id), ) await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_number.test_1") state_2 = hass.states.get("input_number.test_2") state_3 = hass.states.get("input_number.test_3") assert state_1 is not None assert state_2 is not None assert state_3 is None assert float(state_1.state) == 50 assert float(state_2.state) == 20 assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None
def test_if_fires_on_zone_enter(self): """Test for firing on zone enter.""" context = Context() self.hass.states.set( 'geo_location.entity', 'hello', { 'latitude': 32.881011, 'longitude': -117.234758, 'source': 'test_source' }) self.hass.block_till_done() assert setup_component( self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'geo_location', 'source': 'test_source', 'zone': 'zone.test', 'event': 'enter', }, 'action': { 'service': 'test.automation', 'data_template': { 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join( ('platform', 'entity_id', 'from_state.state', 'to_state.state', 'zone.name')) }, } } }) self.hass.states.set('geo_location.entity', 'hello', { 'latitude': 32.880586, 'longitude': -117.237564 }, context=context) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) assert self.calls[0].context is context self.assertEqual( 'geo_location - geo_location.entity - hello - hello - test', self.calls[0].data['some']) # Set out of zone again so we can trigger call self.hass.states.set('geo_location.entity', 'hello', { 'latitude': 32.881011, 'longitude': -117.234758 }) self.hass.block_till_done() common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('geo_location.entity', 'hello', { 'latitude': 32.880586, 'longitude': -117.237564 }) self.hass.block_till_done() self.assertEqual(1, len(self.calls))
def __init__(self, config, user_id, request_id): """Initialize the request data.""" self.config = config self.request_id = request_id self.context = Context(user_id=user_id)
async def test_reload(hass, hass_admin_user): """Test reloading the YAML config.""" assert await async_setup_component( hass, DOMAIN, { DOMAIN: [ { "name": "Person 1", "id": "id-1" }, { "name": "Person 2", "id": "id-2" }, ] }, ) assert len(hass.states.async_entity_ids()) == 2 state_1 = hass.states.get("person.person_1") state_2 = hass.states.get("person.person_2") state_3 = hass.states.get("person.person_3") assert state_1 is not None assert state_1.name == "Person 1" assert state_2 is not None assert state_2.name == "Person 2" assert state_3 is None with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: [ { "name": "Person 1-updated", "id": "id-1" }, { "name": "Person 3", "id": "id-3" }, ] }, ): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_admin_user.id), ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == 2 state_1 = hass.states.get("person.person_1") state_2 = hass.states.get("person.person_2") state_3 = hass.states.get("person.person_3") assert state_1 is not None assert state_1.name == "Person 1-updated" assert state_2 is None assert state_3 is not None assert state_3.name == "Person 3"
async def test_config_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) ent_reg = await entity_registry.async_get_registry(hass) _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) config = { DOMAIN: { "test_1": {}, "test_2": { CONF_NAME: "Hello World", CONF_ICON: "mdi:work", CONF_DURATION: 10, }, } } assert await async_setup_component(hass, "timer", config) await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) await hass.async_block_till_done() state_1 = hass.states.get("timer.test_1") state_2 = hass.states.get("timer.test_2") state_3 = hass.states.get("timer.test_3") assert state_1 is not None assert state_2 is not None assert state_3 is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None assert STATUS_IDLE == state_1.state assert ATTR_ICON not in state_1.attributes assert ATTR_FRIENDLY_NAME not in state_1.attributes assert STATUS_IDLE == state_2.state assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME) assert "mdi:work" == state_2.attributes.get(ATTR_ICON) assert "0:00:10" == state_2.attributes.get(ATTR_DURATION) with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: { "test_2": { CONF_NAME: "Hello World reloaded", CONF_ICON: "mdi:work-reloaded", CONF_DURATION: 20, }, "test_3": {}, } }, ): with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_read_only_user.id), ) await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_admin_user.id), ) await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("timer.test_1") state_2 = hass.states.get("timer.test_2") state_3 = hass.states.get("timer.test_3") assert state_1 is None assert state_2 is not None assert state_3 is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None assert STATUS_IDLE == state_2.state assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME) assert "mdi:work-reloaded" == state_2.attributes.get(ATTR_ICON) assert "0:00:20" == state_2.attributes.get(ATTR_DURATION) assert STATUS_IDLE == state_3.state assert ATTR_ICON not in state_3.attributes assert ATTR_FRIENDLY_NAME not in state_3.attributes
async def async_run( self, run_variables: Optional[_VarsType] = None, context: Optional[Context] = None, started_action: Optional[Callable[..., Any]] = None, ) -> None: """Run script.""" if context is None: self._log("Running script requires passing in a context", level=logging.WARNING) context = Context() if self.is_running: if self.script_mode == SCRIPT_MODE_SINGLE: if self._max_exceeded != "SILENT": self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) return if self.script_mode == SCRIPT_MODE_RESTART: self._log("Restarting") await self.async_stop(update_state=False) elif len(self._runs) == self.max_runs: if self._max_exceeded != "SILENT": self._log( "Maximum number of runs exceeded", level=LOGSEVERITY[self._max_exceeded], ) return # If this is a top level Script then make a copy of the variables in case they # are read-only, but more importantly, so as not to leak any variables created # during the run back to the caller. if self._top_level: if self.variables: try: variables = self.variables.async_render( self._hass, run_variables, ) except template.TemplateError as err: self._log("Error rendering variables: %s", err, level=logging.ERROR) raise elif run_variables: variables = dict(run_variables) else: variables = {} variables["context"] = context else: variables = cast(dict, run_variables) if self.script_mode != SCRIPT_MODE_QUEUED: cls = _ScriptRun else: cls = _QueuedScriptRun run = cls(self._hass, self, cast(dict, variables), context, self._log_exceptions) self._runs.append(run) if started_action: self._hass.async_run_job(started_action) self.last_triggered = utcnow() self._changed() try: await asyncio.shield(run.async_run()) except asyncio.CancelledError: await run.async_stop() self._changed() raise
async def test_reload(hass, hass_admin_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) ent_reg = await entity_registry.async_get_registry(hass) _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "test_1": None, "test_2": { "name": "Hello World", "icon": "mdi:work", "initial": True }, } }, ) _LOGGER.debug("ENTITIES: %s", hass.states.async_entity_ids()) assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_boolean.test_1") state_2 = hass.states.get("input_boolean.test_2") state_3 = hass.states.get("input_boolean.test_3") assert state_1 is not None assert state_2 is not None assert state_3 is None assert STATE_ON == state_2.state assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: { "test_2": { "name": "Hello World reloaded", "icon": "mdi:work_reloaded", "initial": False, }, "test_3": None, } }, ): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_admin_user.id), ) assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_boolean.test_1") state_2 = hass.states.get("input_boolean.test_2") state_3 = hass.states.get("input_boolean.test_3") assert state_1 is None assert state_2 is not None assert state_3 is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None assert STATE_ON == state_2.state # reload is not supposed to change entity state assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME) assert "mdi:work_reloaded" == state_2.attributes.get(ATTR_ICON)
async def test_service_run(hass, caplog): """Test running a service with keyword arguments.""" notify_q = asyncio.Queue(0) await setup_script( hass, notify_q, dt(2020, 7, 1, 11, 59, 59, 999999), """ @service def func1(arg1=1, arg2=2, context=None): x = 1 x = 2 * x + 3 log.info(f"this is func1 x = {x}, arg1 = {arg1}, arg2 = {arg2}") pyscript.done = [x, arg1, arg2, str(context)] # registering twice will cause it to be called twice @service("other.func2") @service("other.func2") @service def func2(**kwargs): x = 1 x = 2 * x + 3 log.info(f"this is func1 x = {x}, kwargs = {kwargs}") has2 = service.has_service("pyscript", "func2") has3 = service.has_service("pyscript", "func3") del kwargs["context"] pyscript.done = [x, kwargs, has2, has3] @service def call_service(domain=None, name=None, **kwargs): if domain == "pyscript" and name == "func1": task.sleep(0) pyscript.func1(**kwargs) else: service.call(domain, name, **kwargs) """, ) context = Context(user_id="1234", parent_id="5678", id="8901") await hass.services.async_call("pyscript", "func1", {}, context=context) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [5, 1, 2, str(context)] assert "this is func1 x = 5" in caplog.text await hass.services.async_call("pyscript", "func1", {"arg1": 10}, context=context) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [5, 10, 2, str(context)] await hass.services.async_call( "pyscript", "call_service", { "domain": "pyscript", "name": "func1", "arg1": "string1" }, context=context, ) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [5, "string1", 2, str(context)] await hass.services.async_call("pyscript", "func1", { "arg1": "string1", "arg2": 123 }, context=context) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [5, "string1", 123, str(context)] await hass.services.async_call("pyscript", "call_service", { "domain": "pyscript", "name": "func2" }) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [5, {"trigger_type": "service"}, 1, 0] await hass.services.async_call( "pyscript", "call_service", { "domain": "pyscript", "name": "func2", "arg1": "string1" }, ) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [ 5, { "trigger_type": "service", "arg1": "string1" }, 1, 0 ] await hass.services.async_call("pyscript", "func2", { "arg1": "string1", "arg2": 456 }) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [ 5, { "trigger_type": "service", "arg1": "string1", "arg2": 456 }, 1, 0 ] await hass.services.async_call("other", "func2", { "arg1": "string1", "arg2": 123 }) ret = await wait_until_done(notify_q) assert literal_eval(ret) == [ 5, { "trigger_type": "service", "arg1": "string1", "arg2": 123 }, 1, 0 ] assert literal_eval(ret) == [ 5, { "trigger_type": "service", "arg1": "string1", "arg2": 123 }, 1, 0 ]
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: conf = config.get(DOMAIN, {}) # type: ConfigType if conf is None: # If we have a config entry, setup is done by that config entry. # If there is no config entry, this should fail. return bool(hass.config_entries.async_entries(DOMAIN)) hass.data[DATA_AIHOME_CONFIG] = conf # Only import if we haven't before. if not hass.config_entries.async_entries(DOMAIN): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={})) if CONF_HTTP in conf: if conf.get(CONF_HTTP) is None: conf[CONF_HTTP] = HTTP_SCHEMA({}) hass.http.register_view(AihomeGateView(hass)) hass.http.register_view( AihomeAuthView( hass, conf.get(CONF_HTTP, {}).get(CONF_HA_URL, hass.config.api.base_url))) global EXPIRATION EXPIRATION = timedelta(hours=conf.get(CONF_HTTP).get( CONF_EXPIRE_IN_HOURS, DEFAULT_EXPIRE_IN_HOURS)) _LOGGER.info('[init] aihome run with "http mode"(mode 1)') MODE.append('http') if CONF_HTTP_PROXY in conf: if conf.get(CONF_HTTP_PROXY) is None: conf[CONF_HTTP_PROXY] = HTTP_PROXY({}) _LOGGER.info('[init] aihome run with "http_proxy mode"(mode 2)') if CONF_SETTING not in conf: _LOGGER.error( '[init] fail to start aihome: http_proxy mode require mqtt congfiguration' ) return False MODE.append('http_proxy') if CONF_SKILL in conf: if conf.get(CONF_SKILL) is None: conf[CONF_SKILL] = SKILL_SCHEMA({}) _LOGGER.info('[init] aihome run with "skill mode"(mode 3)') if CONF_SETTING not in conf: _LOGGER.error( '[init] fail to start aihome: skil mode require mqtt congfiguration' ) return False MODE.append('skill') aihome_util.ENTITY_KEY = conf.get(CONF_SETTING, {}).get(CONF_ENTITY_KEY) aihome_util.CONTEXT_AIHOME = Context( conf.get(CONF_SETTING, {}).get(CONF_USER_ID)) platform = conf[CONF_PLATFORM] manager = hass.data[DATA_AIHOME_BIND_MANAGER] = aihome_util.BindManager( hass, platform) await manager.async_load() for p in platform: try: module = importlib.import_module('custom_components.{}.{}'.format( DOMAIN, p)) _LOGGER.error('[init] import %s.%s', DOMAIN, p) HANDLER[p] = module.createHandler(hass) except ImportError: _LOGGER.error('[init] Unable to import %s.%s', DOMAIN, p) return False return True
async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) ent_reg = er.async_get(hass) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "dt1": { "has_time": False, "has_date": True, "initial": "2019-1-1" }, "dt3": { CONF_HAS_TIME: True, CONF_HAS_DATE: True }, } }, ) assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_datetime.dt1") state_2 = hass.states.get("input_datetime.dt2") state_3 = hass.states.get("input_datetime.dt3") dt_obj = datetime.datetime(2019, 1, 1, 0, 0) assert state_1 is not None assert state_2 is None assert state_3 is not None assert dt_obj.strftime(FMT_DATE) == state_1.state assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt3") == f"{DOMAIN}.dt3" with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: { "dt1": { "has_time": True, "has_date": False, "initial": "23:32" }, "dt2": { "has_time": True, "has_date": True }, } }, ): with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_read_only_user.id), ) await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_admin_user.id), ) assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_datetime.dt1") state_2 = hass.states.get("input_datetime.dt2") state_3 = hass.states.get("input_datetime.dt3") assert state_1 is not None assert state_2 is not None assert state_3 is None assert state_1.state == DEFAULT_TIME.strftime(FMT_TIME) assert state_2.state == datetime.datetime.combine( datetime.date.today(), DEFAULT_TIME).strftime(FMT_DATETIME) assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") == f"{DOMAIN}.dt2" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt3") is None
async def test_confirmable_notification(hass: HomeAssistant) -> None: """Test confirmable notification blueprint.""" with patch_blueprint( "confirmable_notification.yaml", BUILTIN_BLUEPRINT_FOLDER / "confirmable_notification.yaml", ): assert await async_setup_component( hass, script.DOMAIN, { "script": { "confirm": { "use_blueprint": { "path": "confirmable_notification.yaml", "input": { "notify_device": "frodo", "title": "Lord of the things", "message": "Throw ring in mountain?", "confirm_action": [{ "service": "homeassistant.turn_on", "target": { "entity_id": "mount.doom" }, }], }, } } } }, ) turn_on_calls = async_mock_service(hass, "homeassistant", "turn_on") context = Context() with patch( "homeassistant.components.mobile_app.device_action.async_call_action_from_config" ) as mock_call_action: # Trigger script await hass.services.async_call(script.DOMAIN, "confirm", context=context) # Give script the time to attach the trigger. await asyncio.sleep(0.1) hass.bus.async_fire("mobile_app_notification_action", {"action": "ANYTHING_ELSE"}) hass.bus.async_fire("mobile_app_notification_action", {"action": "CONFIRM_" + Context().id}) hass.bus.async_fire("mobile_app_notification_action", {"action": "CONFIRM_" + context.id}) await hass.async_block_till_done() assert len(mock_call_action.mock_calls) == 1 _hass, config, variables, _context = mock_call_action.mock_calls[0][1] template.attach(hass, config) rendered_config = template.render_complex(config, variables) assert rendered_config == { "title": "Lord of the things", "message": "Throw ring in mountain?", "alias": "Send notification", "domain": "mobile_app", "type": "notify", "device_id": "frodo", "data": { "actions": [ { "action": "CONFIRM_" + _context.id, "title": "Confirm" }, { "action": "DISMISS_" + _context.id, "title": "Dismiss" }, ] }, } assert len(turn_on_calls) == 1 assert turn_on_calls[0].data == { "entity_id": ["mount.doom"], }
async def async_trigger(self, run_variables, context=None, skip_condition=False): """Trigger automation. This method is a coroutine. """ reason = "" if "trigger" in run_variables and "description" in run_variables["trigger"]: reason = f' by {run_variables["trigger"]["description"]}' self._logger.debug("Automation triggered%s", reason) # Create a new context referring to the old context. parent_id = None if context is None else context.id trigger_context = Context(parent_id=parent_id) with trace_automation( self.hass, self.unique_id, self._raw_config, self._blueprint_inputs, trigger_context, self._trace_config, ) as automation_trace: if self._variables: try: variables = self._variables.async_render(self.hass, run_variables) except template.TemplateError as err: self._logger.error("Error rendering variables: %s", err) automation_trace.set_error(err) return else: variables = run_variables # Prepare tracing the automation automation_trace.set_trace(trace_get()) # Set trigger reason trigger_description = variables.get("trigger", {}).get("description") automation_trace.set_trigger_description(trigger_description) # Add initial variables as the trigger step if "trigger" in variables and "idx" in variables["trigger"]: trigger_path = f"trigger/{variables['trigger']['idx']}" else: trigger_path = "trigger" trace_element = TraceElement(variables, trigger_path) trace_append_element(trace_element) if ( not skip_condition and self._cond_func is not None and not self._cond_func(variables) ): self._logger.debug( "Conditions not met, aborting automation. Condition summary: %s", trace_get(clear=False), ) script_execution_set("failed_conditions") return self.async_set_context(trigger_context) event_data = { ATTR_NAME: self._name, ATTR_ENTITY_ID: self.entity_id, } if "trigger" in variables and "description" in variables["trigger"]: event_data[ATTR_SOURCE] = variables["trigger"]["description"] @callback def started_action(): self.hass.bus.async_fire( EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) try: with trace_path("action"): await self.action_script.async_run( variables, trigger_context, started_action ) except (vol.Invalid, HomeAssistantError) as err: self._logger.error( "Error while executing automation %s: %s", self.entity_id, err, ) automation_trace.set_error(err) except Exception as err: # pylint: disable=broad-except self._logger.exception("While executing automation %s", self.entity_id) automation_trace.set_error(err)
async def async_setup_entry(hass, config_entry): """Load a config entry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(DATA_HAVCS_HANDLER, {}) hass.data[DOMAIN].setdefault(DATA_HAVCS_CONFIG, {}) hass.data[DOMAIN].setdefault(DATA_HAVCS_ITEMS, {}) hass.data[DOMAIN].setdefault(DATA_HAVCS_SETTINGS, {}) conf = hass.data[DOMAIN].get(DATA_HAVCS_CONFIG) # Config entry was created because user had configuration.yaml entry # They removed that, so remove entry. if config_entry.source == config_entries.SOURCE_IMPORT: if not conf: hass.async_create_task( hass.config_entries.async_remove(config_entry.entry_id)) _LOGGER.info("[init] there is no config in yaml and havcs is managered by yaml, remove config entry. ") return False elif config_entry.source == SOURCE_PLATFORM: if not conf: if [entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.source == config_entries.SOURCE_USER]: return True else: hass.async_create_task( hass.config_entries.async_remove(config_entry.entry_id)) return False else: return True # If user didn't have configuration.yaml config, generate defaults elif config_entry.source == config_entries.SOURCE_USER: if not conf: conf = CONFIG_SCHEMA({DOMAIN: dict(config_entry.data)})[DOMAIN] elif any(key in conf for key in config_entry.data): _LOGGER.warning( "[init] Data in your config entry is going to override your " "configuration.yaml: %s", config_entry.data) for key in config_entry.data: if key in conf: if isinstance(conf[key], dict): conf[key].update(config_entry.data[key]) else: conf[key] = config_entry.data[key] else: conf[key] = config_entry.data[key] if CONF_HTTP not in config_entry.data and CONF_HTTP in conf: conf.pop(CONF_HTTP) if CONF_HTTP_PROXY not in config_entry.data and CONF_HTTP_PROXY in conf: conf.pop(CONF_HTTP_PROXY) if CONF_SKILL not in config_entry.data and CONF_SKILL in conf: conf.pop(CONF_SKILL) conf = CONFIG_SCHEMA({DOMAIN: conf})[DOMAIN] http_manager = hass.data[DOMAIN][DATA_HAVCS_HTTP_MANAGER] = HavcsHttpManager(hass, conf.get(CONF_HTTP, {}).get(CONF_HA_URL, get_url(hass)), DEVICE_CONFIG_SCHEMA, SETTINGS_CONFIG_SCHEMA) if CONF_HTTP in conf: if conf.get(CONF_HTTP) is None: conf[CONF_HTTP] = HTTP_SCHEMA({}) http_manager.set_expiration(timedelta(hours=conf.get(CONF_HTTP).get(CONF_EXPIRE_IN_HOURS, DEFAULT_EXPIRE_IN_HOURS))) http_manager.register_auth_authorize() http_manager.register_auth_token() http_manager.register_service() _LOGGER.info("[init] havcs enable \"http mode\"") MODE.append('http') if CONF_HTTP_PROXY in conf: if conf.get(CONF_HTTP_PROXY) is None: conf[CONF_HTTP_PROXY] = HTTP_PROXY({}) _LOGGER.info("[init] havcs enable \"http_proxy mode\"") if CONF_SETTING not in conf: _LOGGER.error("[init] fail to start havcs: http_proxy mode require mqtt congfiguration") return False MODE.append('http_proxy') if CONF_SKILL in conf: if conf.get(CONF_SKILL) is None: conf[CONF_SKILL] = SKILL_SCHEMA({}) _LOGGER.info("[init] havcs enable \"skill mode\"") if CONF_SETTING not in conf: _LOGGER.error("[init] fail to start havcs: skill mode require mqtt congfiguration") return False MODE.append('skill') havcs_util.ENTITY_KEY = conf.get(CONF_SETTING, {}).get(CONF_ENTITY_KEY) havcs_util.CONTEXT_HAVCS = Context(conf.get(CONF_SETTING, {}).get(CONF_USER_ID)) platforms = conf.get(CONF_PLATFORM) device_config = conf.get(CONF_DEVICE_CONFIG) if device_config == 'text': havc_device_config_path = os.path.join(hass.config.config_dir, 'havcs.yaml') if not os.path.isfile(havc_device_config_path): with open(havc_device_config_path, "wt") as havc_device_config_file: havc_device_config_file.write('') hass.components.frontend.async_remove_panel(DOMAIN) else: havc_device_config_path = os.path.join(hass.config.config_dir, 'havcs-ui.yaml') if not os.path.isfile(havc_device_config_path): if os.path.isfile(os.path.join(hass.config.config_dir, 'havcs.yaml')): shutil.copyfile(os.path.join(hass.config.config_dir, 'havcs.yaml'), havc_device_config_path) else: with open(havc_device_config_path, "wt") as havc_device_config_file: havc_device_config_file.write('') http_manager.register_deivce_manager() hass.data[DOMAIN][CONF_DEVICE_CONFIG_PATH] = havc_device_config_path havc_settings_config_path = os.path.join(hass.config.config_dir, 'havcs-settings.yaml') if not os.path.isfile(havc_settings_config_path): with open(havc_settings_config_path, "wt") as havc_settings_config_file: havc_settings_config_file.write('') http_manager.register_settings_manager() hass.data[DOMAIN][CONF_SETTINGS_CONFIG_PATH] = havc_settings_config_path sync_device = conf.get(CONF_SKILL, {}).get(CONF_SYNC_DEVICE) bind_device = conf.get(CONF_SKILL, {}).get(CONF_BIND_DEVICE) if CONF_HTTP_PROXY not in conf and CONF_SKILL not in conf: _LOGGER.debug("[init] havcs only run in http mode, skip mqtt initialization") ha_url = conf.get(CONF_HTTP, {}).get(CONF_HA_URL, get_url(hass)) _LOGGER.debug("[init] ha_url = %s, base_url = %s", ha_url, get_url(hass)) else: setting_conf = conf.get(CONF_SETTING) app_key = setting_conf.get(CONF_APP_KEY) app_secret = setting_conf.get(CONF_APP_SECRET) decrypt_key =bytes().fromhex(sha1(app_secret.encode("utf-8")).hexdigest())[0:16] if platforms: bind_manager = hass.data[DOMAIN][DATA_HAVCS_BIND_MANAGER] = HavcsBindManager(hass, platforms, bind_device, sync_device, app_key, decrypt_key) await bind_manager.async_init() allowed_uri = conf.get(CONF_HTTP_PROXY, {}).get(CONF_ALLOWED_URI) ha_url = conf.get(CONF_HTTP_PROXY, {}).get(CONF_HA_URL, get_url(hass)) # 组装mqtt配置 mqtt_conf = {} for (k,v) in setting_conf.items(): if(k == CONF_APP_KEY): mqtt_conf.setdefault('username', v) elif(k == CONF_APP_SECRET): mqtt_conf.setdefault('password', v) elif(k == CONF_ENTITY_KEY): continue else: mqtt_conf.setdefault(k, v) certificate = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ca.crt') if os.path.exists(certificate): mqtt_conf[CONF_CERTIFICATE] = certificate _LOGGER.debug("[init] sucess to autoload ca.crt from %s", certificate) validate_mqtt_conf = mqtt.CONFIG_SCHEMA({'mqtt': mqtt_conf})['mqtt'] hass.data[DOMAIN][DATA_HAVCS_MQTT] = mqtt.MQTT( hass, config_entry, mqtt_conf ) _LOGGER.debug("[init] connecting to mqtt server") await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_connect() # 没有返回值 retry = 5 _LOGGER.debug("[init] wait for mqtt client connection status") while retry > 0 : await asyncio.sleep(5) if hass.data[DOMAIN][DATA_HAVCS_MQTT].connected: _LOGGER.debug("[init] mqtt client connected") break retry -= 1 _LOGGER.debug("[init] mqtt client not connected yet, check after 5s") if hass.data[DOMAIN][DATA_HAVCS_MQTT].connected: pass else: import hashlib md5_l = hashlib.md5() with open(certificate,mode="rb") as f: by = f.read() md5_l.update(by) local_ca_md5 = md5_l.hexdigest() _LOGGER.debug("[init] local ca.crt md5 %s", local_ca_md5) try: ca_url = 'https://raw.githubusercontent.com/cnk700i/havcs/master/custom_components/havcs/ca.crt' session = async_get_clientsession(hass, verify_ssl=False) with async_timeout.timeout(5, loop=hass.loop): response = await session.get(ca_url) ca_bytes = await response.read() md5_l = hashlib.md5() md5_l.update(ca_bytes) latest_ca_md5 = md5_l.hexdigest() _LOGGER.debug("[init] remote ca.crt md5 %s", latest_ca_md5) if local_ca_md5 != latest_ca_md5: _LOGGER.error("[init] can not connect to mqtt server(host = %s, port = %s), try update ca.crt file ",setting_conf[CONF_BROKER], setting_conf[CONF_PORT]) else: _LOGGER.error("[init] can not connect to mqtt server(host = %s, port = %s), check mqtt server's address and port ", setting_conf[CONF_BROKER], setting_conf[CONF_PORT]) except Exception as e: _LOGGER.error("[init] fail to check whether ca.crt is latest, cause by %s", repr(e)) _LOGGER.error("[init] fail to init havcs") await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_disconnect() return False async def async_stop_mqtt(event: Event): """Stop MQTT component.""" await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_disconnect() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) async def async_http_proxy_handler(mqtt_msg, topic, start_time = None): response = None url = ha_url + mqtt_msg['uri'] _LOGGER.debug("[http_proxy] request: url = %s", url) if('content' in mqtt_msg): _LOGGER.debug("[http_proxy] use POST method") platform = mqtt_msg.get('platform', havcs_util.get_platform_from_command(mqtt_msg['content'])) auth_type, auth_value = mqtt_msg.get('headers', {}).get('Authorization',' ').split(' ', 1) _LOGGER.debug("[http_proxy] platform = %s, auth_type = %s, access_token = %s", platform, auth_type, auth_value) try: session = async_get_clientsession(hass, verify_ssl=False) with async_timeout.timeout(5, loop=hass.loop): response = await session.post(url, data=mqtt_msg['content'], headers = mqtt_msg.get('headers')) except(asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("[http_proxy] fail to access %s in local network: timeout", url) except: _LOGGER.error("[http_proxy] fail to access %s in local network: %s", url, traceback.format_exc()) else: _LOGGER.debug("[http_proxy] use GET method") try: session = async_get_clientsession(hass, verify_ssl=False) with async_timeout.timeout(5, loop=hass.loop): response = await session.get(url, headers = mqtt_msg.get('headers')) except(asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("[http_proxy] fail to access %s in local network: timeout", url) except: _LOGGER.error("[http_proxy] fail to access %s in local network: %s", url, traceback.format_exc()) # _LOGGER.debug("[http_proxy] %s", response.history) #查看重定向信息 if response is not None: if response.status != 200: _LOGGER.error("[http_proxy] fail to access %s in local network: status=%d",url,response.status) if('image' in response.headers['Content-Type'] or 'stream' in response.headers['Content-Type']): result = await response.read() result = b64encode(result).decode() else: result = await response.text() headers = { 'Content-Type': response.headers['Content-Type'] + ';charset=utf-8' } res = { 'headers': headers, 'status': response.status, 'content': result.encode('utf-8').decode('unicode_escape'), 'msgId': mqtt_msg.get('msgId') } _LOGGER.debug("[http_proxy] response: uri = %s, msgid = %s, type = %s", mqtt_msg['uri'].split('?')[0], mqtt_msg.get('msgId'), response.headers['Content-Type']) else: res = { 'status': 500, 'content': '{"error":"time_out"}', 'msgId': mqtt_msg.get('msgId') } _LOGGER.debug("[http_proxy] response: uri = %s, msgid = %s", mqtt_msg['uri'].split('?')[0], mqtt_msg.get('msgId')) res = havcs_util.AESCipher(decrypt_key).encrypt(json.dumps(res, ensure_ascii = False).encode('utf8')) await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_publish(topic.replace('/request/','/response/'), res, 2, False) end_time = datetime.now() _LOGGER.debug("[mqtt] -------- mqtt task finish at %s, Running time: %ss --------", end_time.strftime('%Y-%m-%d %H:%M:%S'), (end_time - start_time).total_seconds()) async def async_module_handler(mqtt_msg, topic, start_time = None): platform = mqtt_msg.get('platform', havcs_util.get_platform_from_command(mqtt_msg['content'])) if platform == 'unknown': _LOGGER.error("[skill] receive command from unsupport platform \"%s\"", platform) return if platform not in hass.data[DOMAIN][DATA_HAVCS_HANDLER]: _LOGGER.error("[skill] receive command from uninitialized platform \"%s\" , check up your configuration.yaml", platform) return try: response = await hass.data[DOMAIN][DATA_HAVCS_HANDLER][platform].handleRequest(json.loads(mqtt_msg['content']), auth = True, request_from = "mqtt") except: response = '{"error":"service error"}' _LOGGER.error("[skill] %s", traceback.format_exc()) res = { 'headers': {'Content-Type': 'application/json;charset=utf-8'}, 'status': 200, 'content': json.dumps(response).encode('utf-8').decode('unicode_escape'), 'msgId': mqtt_msg.get('msgId') } res = havcs_util.AESCipher(decrypt_key).encrypt(json.dumps(res, ensure_ascii = False).encode('utf8')) await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_publish(topic.replace('/request/','/response/'), res, 2, False) end_time = datetime.now() _LOGGER.debug("[mqtt] -------- mqtt task finish at %s, Running time: %ss --------", end_time.strftime('%Y-%m-%d %H:%M:%S'), (end_time - start_time).total_seconds()) async def async_publish_error(mqtt_msg,topic): res = { 'headers': {'Content-Type': 'application/json;charset=utf-8'}, 'status': 404, 'content': '', 'msgId': mqtt_msg.get('msgId') } res = havcs_util.AESCipher(decrypt_key).encrypt(json.dumps(res, ensure_ascii = False).encode('utf8')) await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_publish(topic.replace('/request/','/response/'), res, 2, False) @callback def message_received(*args): # 0.90 传参变化 if isinstance(args[0], str): topic = args[0] payload = args[1] # qos = args[2] else: topic = args[0].topic payload = args[0].payload # qos = args[0].qos """Handle new MQTT state messages.""" # _LOGGER.debug("[mqtt] get encrypt message: \n {}".format(payload)) try: start_time = datetime.now() end_time = None _LOGGER.debug("[mqtt] -------- start handle task from mqtt at %s --------", start_time.strftime('%Y-%m-%d %H:%M:%S')) payload = havcs_util.AESCipher(decrypt_key).decrypt(payload) # _LOGGER.debug("[mqtt] get raw message: \n {}".format(payload)) req = json.loads(payload) if req.get('msgType') == 'hello': _LOGGER.info("[mqtt] get hello message: %s", req.get('content')) end_time = datetime.now() _LOGGER.debug("[mqtt] -------- mqtt task finish at %s, Running time: %ss --------", end_time.strftime('%Y-%m-%d %H:%M:%S'), (end_time - start_time).total_seconds()) return _LOGGER.debug("[mqtt] raw message: %s", req) if req.get('platform') == 'h2m2h': if('http_proxy' not in MODE): _LOGGER.info("[http_proxy] havcs not run in http_proxy mode, ignore request: %s", req) raise RuntimeError if(allowed_uri and req.get('uri','/').split('?')[0] not in allowed_uri): _LOGGER.info("[http_proxy] uri not allowed: %s", req.get('uri','/')) hass.add_job(async_publish_error(req, topic)) raise RuntimeError hass.add_job(async_http_proxy_handler(req, topic, start_time)) else: if('skill' not in MODE): _LOGGER.info("[skill] havcs not run in skill mode, ignore request: %s", req) raise RuntimeError hass.add_job(async_module_handler(req, topic, start_time)) except (json.decoder.JSONDecodeError, UnicodeDecodeError, binascii.Error): import sys ex_type, ex_val, ex_stack = sys.exc_info() log = '' for stack in traceback.extract_tb(ex_stack): log += str(stack) _LOGGER.debug("[mqtt] fail to decrypt message, abandon[%s][%s]: %s", ex_type, ex_val, log) end_time = datetime.now() except: _LOGGER.error("[mqtt] fail to handle %s", traceback.format_exc()) end_time = datetime.now() if end_time: _LOGGER.debug("[mqtt] -------- mqtt task finish at %s, Running time: %ss --------", end_time.strftime('%Y-%m-%d %H:%M:%S'), (end_time - start_time).total_seconds()) await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_subscribe("ai-home/http2mqtt2hass/"+app_key+"/request/#", message_received, 2, 'utf-8') _LOGGER.info("[init] mqtt initialization finished, waiting for welcome message of mqtt server.") async def start_havcs(event: Event): async def async_load_settings(): _LOGGER.info("loading settings from file") try: settings_config = await hass.async_add_executor_job( conf_util.load_yaml_config_file, havc_settings_config_path ) hass.data[DOMAIN][DATA_HAVCS_SETTINGS] = SETTINGS_CONFIG_SCHEMA(settings_config) except HomeAssistantError as err: _LOGGER.error("Error loading %s: %s", havc_settings_config_path, err) return None except vol.error.Error as exception: _LOGGER.warning("failed to load all settings from file, find invalid data: %s", exception) except: _LOGGER.error("Error loading %s: %s", havc_settings_config_path, traceback.format_exc()) return None await async_load_settings() async def async_load_device_info(): _LOGGER.info("loading device info from file") try: device_config = await hass.async_add_executor_job( conf_util.load_yaml_config_file, havc_device_config_path ) hass.data[DOMAIN][DATA_HAVCS_ITEMS] = DEVICE_CONFIG_SCHEMA(device_config) except HomeAssistantError as err: _LOGGER.error("Error loading %s: %s", havc_device_config_path, err) return None except vol.error.Error as exception: _LOGGER.warning("failed to load all devices from file, find invalid data: %s", exception) except: _LOGGER.error("Error loading %s: %s", havc_device_config_path, traceback.format_exc()) return None async def async_init_sub_entry(): # create when config change mode = [] if CONF_HTTP in conf: mode.append(CONF_HTTP) if CONF_HTTP_PROXY in conf: mode.append(CONF_HTTP_PROXY) if CONF_SKILL in conf: mode.append(CONF_SKILL) havcs_entries = hass.config_entries.async_entries(DOMAIN) # sub entry for every platform entry_platforms = set([entry.data.get('platform') for entry in havcs_entries if entry.source == SOURCE_PLATFORM]) conf_platforms = set(conf.get(CONF_PLATFORM)) new_platforms = conf_platforms - entry_platforms _LOGGER.debug("[post-task] load new platform entry %s", new_platforms) for platform in new_platforms: # 如果在async_setup_entry中执行无法await,async_init所触发的component_setup会不断等待之前的component_setup任务 await hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': SOURCE_PLATFORM}, data={'platform': platform, 'mode': mode} )) remove_platforms = entry_platforms - conf_platforms _LOGGER.debug("[post-task] remove old platform entry %s", remove_platforms) for entry in [entry for entry in havcs_entries if entry.source == SOURCE_PLATFORM]: if entry.data.get('platform') in remove_platforms: await hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) else: entry.title=f"接入平台[{entry.data.get('platform')}-{DEVICE_PLATFORM_DICT[entry.data.get('platform')]['cn_name']}],接入方式{mode}" hass.config_entries.async_update_entry(entry) # await async_load_device_info() for platform in platforms: for ent in hass.config_entries.async_entries(DOMAIN): if ent.source == SOURCE_PLATFORM and ent.data.get('platform') == platform: try: module = importlib.import_module('custom_components.{}.{}'.format(DOMAIN,platform)) _LOGGER.info("[post-task] import %s.%s", DOMAIN, platform) hass.data[DOMAIN][DATA_HAVCS_HANDLER][platform] = await module.createHandler(hass, ent) # hass.data[DOMAIN][DATA_HAVCS_HANDLER][platform].vcdm.all(hass, True) except ImportError as err: _LOGGER.error("[post-task] Unable to import %s.%s, %s", DOMAIN, platform, err) return False except: _LOGGER.error("[post-task] fail to create %s handler: %s", platform , traceback.format_exc()) return False break await async_init_sub_entry() if DATA_HAVCS_MQTT in hass.data[DOMAIN]: await hass.data[DOMAIN][DATA_HAVCS_MQTT].async_publish("ai-home/http2mqtt2hass/"+app_key+"/response/test", 'init', 2, False) async def async_handler_service(service): if service.service == SERVICE_RELOAD: await async_load_device_info() for platform in hass.data[DOMAIN][DATA_HAVCS_HANDLER]: devices = hass.data[DOMAIN][DATA_HAVCS_HANDLER][platform].vcdm.all(hass, True) await hass.data[DOMAIN][DATA_HAVCS_HANDLER][platform].vcdm.async_reregister_devices(hass) _LOGGER.info("[service] ------------%s 平台加载设备信息------------\n%s", platform, [device.attributes for device in devices]) mind_devices = [device.attributes for device in devices if None in device.attributes.values() or [] in device.attributes.values()] if mind_devices: _LOGGER.debug("!!!!!!!! 以下设备信息不完整,检查值为None或[]的属性并进行设置 !!!!!!!!") for mind_device in mind_devices: _LOGGER.debug("%s", mind_device) _LOGGER.info("[service] ------------%s 平台加载设备信息------------\n", platform) if bind_device: await hass.data[DOMAIN][DATA_HAVCS_BIND_MANAGER].async_bind_device() elif service.service == SERVICE_DEBUG_DISCOVERY: for platform, handler in hass.data[DOMAIN][DATA_HAVCS_HANDLER].items(): err_result, discovery_devices, entity_ids = handler.process_discovery_command("service_call") _LOGGER.info("[service][%s] trigger discovery command, response: %s", platform, discovery_devices) else: pass hass.services.async_register(DOMAIN, SERVICE_RELOAD, async_handler_service, schema=HAVCS_SERVICE_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_DEBUG_DISCOVERY, async_handler_service, schema=HAVCS_SERVICE_SCHEMA) await hass.services.async_call(DOMAIN, SERVICE_RELOAD) if CONF_HTTP in conf or CONF_HTTP_PROXY in conf: hass.async_create_task(http_manager.async_check_http_oauth()) if config_entry.source == config_entries.SOURCE_IMPORT: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_havcs) elif config_entry.source == config_entries.SOURCE_USER: hass.async_create_task(start_havcs(None)) _LOGGER.info("[init] havcs initialization finished.") return True
async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) ent_reg = await entity_registry.async_get_registry(hass) assert await async_setup_component( hass, DOMAIN, { DOMAIN: { "test_1": { "options": ["first option", "middle option", "last option"], "initial": "middle option", }, "test_2": { "options": ["an option", "not an option"], "initial": "an option", }, } }, ) assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_select.test_1") state_2 = hass.states.get("input_select.test_2") state_3 = hass.states.get("input_select.test_3") assert state_1 is not None assert state_2 is not None assert state_3 is None assert "middle option" == state_1.state assert "an option" == state_2.state assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: { "test_2": { "options": ["an option", "reloaded option"], "initial": "reloaded option", }, "test_3": { "options": ["new option", "newer option"], "initial": "newer option", }, } }, ): with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_read_only_user.id), ) await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_admin_user.id), ) await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_select.test_1") state_2 = hass.states.get("input_select.test_2") state_3 = hass.states.get("input_select.test_3") assert state_1 is None assert state_2 is not None assert state_3 is not None assert "an option" == state_2.state assert "newer option" == state_3.state assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None
def registration_context(registration: dict) -> Context: """Generate a context from a request.""" return Context(user_id=registration[CONF_USER_ID])
async def test_configure(hass, hass_admin_user): """Test that setting values through configure works.""" assert await async_setup_component( hass, "counter", {"counter": { "test": { "maximum": "10", "initial": "10" } }}) state = hass.states.get("counter.test") assert state is not None assert state.state == "10" assert 10 == state.attributes.get("maximum") # update max await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "maximum": 0 }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "0" assert 0 == state.attributes.get("maximum") # disable max await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "maximum": None }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "0" assert state.attributes.get("maximum") is None # update min assert state.attributes.get("minimum") is None await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "minimum": 5 }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "5" assert 5 == state.attributes.get("minimum") # disable min await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "minimum": None }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "5" assert state.attributes.get("minimum") is None # update step assert 1 == state.attributes.get("step") await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "step": 3 }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "5" assert 3 == state.attributes.get("step") # update value await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "value": 6 }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "6" # update initial await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "initial": 5 }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "6" assert 5 == state.attributes.get("initial") # update all await hass.services.async_call( "counter", "configure", { "entity_id": state.entity_id, "step": 5, "minimum": 0, "maximum": 9, "value": 5, "initial": 6, }, True, Context(user_id=hass_admin_user.id), ) state = hass.states.get("counter.test") assert state is not None assert state.state == "5" assert 5 == state.attributes.get("step") assert 0 == state.attributes.get("minimum") assert 9 == state.attributes.get("maximum") assert 6 == state.attributes.get("initial")
async def test_if_fires_on_zone_enter(hass, calls): """Test for firing on zone enter.""" context = Context() hass.states.async_set("test.entity", "hello", { "latitude": 32.881011, "longitude": -117.234758 }) await hass.async_block_till_done() assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: { "trigger": { "platform": "zone", "entity_id": "test.entity", "zone": "zone.test", "event": "enter", }, "action": { "service": "test.automation", "data_template": { "some": "{{ trigger.%s }}" % "}} - {{ trigger.".join(( "platform", "entity_id", "from_state.state", "to_state.state", "zone.name", )) }, }, } }, ) hass.states.async_set( "test.entity", "hello", { "latitude": 32.880586, "longitude": -117.237564 }, context=context, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert "zone - test.entity - hello - hello - test" == calls[0].data["some"] # Set out of zone again so we can trigger call hass.states.async_set("test.entity", "hello", { "latitude": 32.881011, "longitude": -117.234758 }) await hass.async_block_till_done() await common.async_turn_off(hass) await hass.async_block_till_done() hass.states.async_set("test.entity", "hello", { "latitude": 32.880586, "longitude": -117.237564 }) await hass.async_block_till_done() assert len(calls) == 1
async def test_if_fires_on_zone_enter(hass, calls): """Test for firing on zone enter.""" context = Context() hass.states.async_set('geo_location.entity', 'hello', { 'latitude': 32.881011, 'longitude': -117.234758, 'source': 'test_source' }) await hass.async_block_till_done() assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'geo_location', 'source': 'test_source', 'zone': 'zone.test', 'event': 'enter', }, 'action': { 'service': 'test.automation', 'data_template': { 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join( ('platform', 'entity_id', 'from_state.state', 'to_state.state', 'zone.name')) }, } } }) hass.states.async_set('geo_location.entity', 'hello', { 'latitude': 32.880586, 'longitude': -117.237564 }, context=context) await hass.async_block_till_done() assert 1 == len(calls) assert calls[0].context is context assert 'geo_location - geo_location.entity - hello - hello - test' == \ calls[0].data['some'] # Set out of zone again so we can trigger call hass.states.async_set('geo_location.entity', 'hello', { 'latitude': 32.881011, 'longitude': -117.234758 }) await hass.async_block_till_done() await common.async_turn_off(hass) await hass.async_block_till_done() hass.states.async_set('geo_location.entity', 'hello', { 'latitude': 32.880586, 'longitude': -117.237564 }) await hass.async_block_till_done() assert 1 == len(calls)
def context(self) -> Context: # type: ignore[override] """State context.""" if self._context is None: self._context = Context(id=None) # type: ignore[arg-type] return self._context
async def test_shared_context(hass, calls): """Test that the shared context is passed down the chain.""" assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: [ { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": {"event": "test_event2"}, }, { "alias": "bye", "trigger": {"platform": "event", "event_type": "test_event2"}, "action": {"service": "test.automation"}, }, ] }, ) context = Context() first_automation_listener = Mock() event_mock = Mock() hass.bus.async_listen("test_event2", first_automation_listener) hass.bus.async_listen(EVENT_AUTOMATION_TRIGGERED, event_mock) hass.bus.async_fire("test_event", context=context) await hass.async_block_till_done() # Ensure events was fired assert first_automation_listener.call_count == 1 assert event_mock.call_count == 2 # Verify automation triggered evenet for 'hello' automation args, _ = event_mock.call_args_list[0] first_trigger_context = args[0].context assert first_trigger_context.parent_id == context.id # Ensure event data has all attributes set assert args[0].data.get(ATTR_NAME) is not None assert args[0].data.get(ATTR_ENTITY_ID) is not None assert args[0].data.get(ATTR_SOURCE) is not None # Ensure context set correctly for event fired by 'hello' automation args, _ = first_automation_listener.call_args assert args[0].context is first_trigger_context # Ensure the 'hello' automation state has the right context state = hass.states.get("automation.hello") assert state is not None assert state.context is first_trigger_context # Verify automation triggered evenet for 'bye' automation args, _ = event_mock.call_args_list[1] second_trigger_context = args[0].context assert second_trigger_context.parent_id == first_trigger_context.id # Ensure event data has all attributes set assert args[0].data.get(ATTR_NAME) is not None assert args[0].data.get(ATTR_ENTITY_ID) is not None assert args[0].data.get(ATTR_SOURCE) is not None # Ensure the service call from the second automation # shares the same context assert len(calls) == 1 assert calls[0].context is second_trigger_context
async def test_if_fires_on_entity_creation_and_removal(hass, calls): """Test for firing on entity creation and removal, with to/from constraints.""" # set automations for multiple combinations to/from assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: [ { "trigger": { "platform": "state", "entity_id": "test.entity_0" }, "action": { "service": "test.automation" }, }, { "trigger": { "platform": "state", "from": "hello", "entity_id": "test.entity_1", }, "action": { "service": "test.automation" }, }, { "trigger": { "platform": "state", "to": "world", "entity_id": "test.entity_2", }, "action": { "service": "test.automation" }, }, ], }, ) await hass.async_block_till_done() # use contexts to identify trigger entities context_0 = Context() context_1 = Context() context_2 = Context() # automation with match_all triggers on creation hass.states.async_set("test.entity_0", "any", context=context_0) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].context.parent_id == context_0.id # create entities, trigger on test.entity_2 ('to' matches, no 'from') hass.states.async_set("test.entity_1", "hello", context=context_1) hass.states.async_set("test.entity_2", "world", context=context_2) await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].context.parent_id == context_2.id # removal of both, trigger on test.entity_1 ('from' matches, no 'to') assert hass.states.async_remove("test.entity_1", context=context_1) assert hass.states.async_remove("test.entity_2", context=context_2) await hass.async_block_till_done() assert len(calls) == 3 assert calls[2].context.parent_id == context_1.id # automation with match_all triggers on removal assert hass.states.async_remove("test.entity_0", context=context_0) await hass.async_block_till_done() assert len(calls) == 4 assert calls[3].context.parent_id == context_0.id
async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) ent_reg = await entity_registry.async_get_registry(hass) assert await setup.async_setup_component( hass, DOMAIN, { DOMAIN: [ { "name": "yaml 1", "latitude": 1, "longitude": 2 }, { "name": "yaml 2", "latitude": 3, "longitude": 4 }, ], }, ) assert count_start + 3 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("zone.yaml_1") state_2 = hass.states.get("zone.yaml_2") state_3 = hass.states.get("zone.yaml_3") assert state_1 is not None assert state_1.attributes["latitude"] == 1 assert state_1.attributes["longitude"] == 2 assert state_2 is not None assert state_2.attributes["latitude"] == 3 assert state_2.attributes["longitude"] == 4 assert state_3 is None assert len(ent_reg.entities) == 0 with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: [ { "name": "yaml 2", "latitude": 3, "longitude": 4 }, { "name": "yaml 3", "latitude": 5, "longitude": 6 }, ] }, ): with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_read_only_user.id), ) await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, context=Context(user_id=hass_admin_user.id), ) await hass.async_block_till_done() assert count_start + 3 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("zone.yaml_1") state_2 = hass.states.get("zone.yaml_2") state_3 = hass.states.get("zone.yaml_3") assert state_1 is None assert state_2 is not None assert state_2.attributes["latitude"] == 3 assert state_2.attributes["longitude"] == 4 assert state_3 is not None assert state_3.attributes["latitude"] == 5 assert state_3.attributes["longitude"] == 6
def context(self): """State context.""" if not self._context: self._context = Context(id=None) return self._context
def context(self, msg): """Return a context.""" user = self.user if user is None: return Context() return Context(user_id=user.id)
async def test_service_call_params(hass): """Test that hass params get set properly on service calls.""" with patch.object(hass.services, "async_call") as call, patch.object( Function, "service_has_service", return_value=True): Function.init(hass) await Function.service_call("test", "test", context=Context(id="test"), blocking=True, limit=1, other_service_data="test") assert call.called assert call.call_args[0] == ("test", "test", { "other_service_data": "test" }) assert call.call_args[1] == { "context": Context(id="test"), "blocking": True, "limit": 1 } call.reset_mock() await Function.service_call("test", "test", context=Context(id="test"), blocking=False, other_service_data="test") assert call.called assert call.call_args[0] == ("test", "test", { "other_service_data": "test" }) assert call.call_args[1] == { "context": Context(id="test"), "blocking": False } call.reset_mock() await Function.get("test.test")(context=Context(id="test"), blocking=True, limit=1, other_service_data="test") assert call.called assert call.call_args[0] == ("test", "test", { "other_service_data": "test" }) assert call.call_args[1] == { "context": Context(id="test"), "blocking": True, "limit": 1 } call.reset_mock() await Function.get("test.test")(context=Context(id="test"), blocking=False, other_service_data="test") assert call.called assert call.call_args[0] == ("test", "test", { "other_service_data": "test" }) assert call.call_args[1] == { "context": Context(id="test"), "blocking": False } # Stop all tasks to avoid conflicts with other tests await Function.waiter_stop() await Function.reaper_stop()