async def fetch_blueprint_from_github_url(hass: HomeAssistant, url: str) -> ImportedBlueprint: """Get a blueprint from a github url.""" import_url = _get_github_import_url(url) session = aiohttp_client.async_get_clientsession(hass) resp = await session.get(import_url, raise_for_status=True) raw_yaml = await resp.text() data = yaml.parse_yaml(raw_yaml) blueprint = Blueprint(data) parsed_import_url = yarl.URL(import_url) suggested_filename = f"{parsed_import_url.parts[1]}-{parsed_import_url.parts[-1]}" if suggested_filename.endswith(".yaml"): suggested_filename = suggested_filename[:-5] return ImportedBlueprint(url, suggested_filename, raw_yaml, blueprint)
def _extract_blueprint_from_community_topic( url: str, topic: dict, ) -> ImportedBlueprint: """Extract a blueprint from a community post JSON. Async friendly. """ block_content: str blueprint = None post = topic["post_stream"]["posts"][0] for match in COMMUNITY_CODE_BLOCK.finditer(post["cooked"]): block_syntax, block_content = match.groups() if block_syntax not in ("auto", "yaml"): continue block_content = html.unescape(block_content.strip()) try: data = yaml.parse_yaml(block_content) except HomeAssistantError: if block_syntax == "yaml": raise continue if not is_blueprint_config(data): continue assert isinstance(data, dict) blueprint = Blueprint(data) break if blueprint is None: raise HomeAssistantError( "No valid blueprint found in the topic. Blueprint syntax blocks need to be marked as YAML or no syntax." ) return ImportedBlueprint(f'{post["username"]}/{topic["slug"]}', block_content, blueprint)
async def fetch_blueprint_from_github_gist_url( hass: HomeAssistant, url: str ) -> ImportedBlueprint: """Get a blueprint from a Github Gist.""" if not url.startswith("https://gist.github.com/"): raise UnsupportedUrl("Not a GitHub gist url") parsed_url = yarl.URL(url) session = aiohttp_client.async_get_clientsession(hass) resp = await session.get( f"https://api.github.com/gists/{parsed_url.parts[2]}", headers={"Accept": "application/vnd.github.v3+json"}, raise_for_status=True, ) gist = await resp.json() blueprint = None filename = None content = None for filename, info in gist["files"].items(): if not filename.endswith(".yaml"): continue content = info["content"] data = yaml.parse_yaml(content) if not is_blueprint_config(data): continue blueprint = Blueprint(data) break if blueprint is None: raise HomeAssistantError( "No valid blueprint found in the gist. The blueprint file needs to end with '.yaml'" ) return ImportedBlueprint( f"{gist['owner']['login']}/{filename[:-5]}", content, blueprint )
async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): """Test saving blueprints.""" raw_data = Path( hass.config.path( "blueprints/automation/test_event_service.yaml")).read_text() with patch("pathlib.Path.write_text") as write_mock: client = await hass_ws_client(hass) await client.send_json({ "id": 6, "type": "blueprint/save", "path": "test_save", "yaml": raw_data, "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", }) msg = await client.receive_json() assert msg["id"] == 6 assert msg["success"] assert write_mock.mock_calls # There are subtle differences in the dumper quoting # behavior when quoting is not required as both produce # valid yaml output_yaml = write_mock.call_args[0][0] assert output_yaml in ( # pure python dumper will quote the value after !input "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n" # c dumper will not quote the value after !input "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n entity_id: light.kitchen\n" ) # Make sure ita parsable and does not raise assert len(parse_yaml(output_yaml)) > 1
def _extract_blueprint_from_community_topic( url: str, topic: dict, ) -> Optional[ImportedBlueprint]: """Extract a blueprint from a community post JSON. Async friendly. """ block_content = None blueprint = None post = topic["post_stream"]["posts"][0] for match in COMMUNITY_CODE_BLOCK.finditer(post["cooked"]): block_syntax, block_content = match.groups() if block_syntax not in ("auto", "yaml"): continue block_content = block_content.strip() try: data = yaml.parse_yaml(block_content) except HomeAssistantError: if block_syntax == "yaml": raise continue if not is_blueprint_config(data): continue blueprint = Blueprint(data) break if blueprint is None: return None return ImportedBlueprint(url, topic["slug"], block_content, blueprint)
async def ws_save_blueprint(hass, connection, msg): """Save a blueprint.""" path = msg["path"] domain = msg["domain"] domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get( DOMAIN, {} ) if domain not in domain_blueprints: connection.send_error( msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain" ) try: blueprint = models.Blueprint( yaml.parse_yaml(msg["yaml"]), expected_domain=domain ) if "source_url" in msg: blueprint.update_metadata(source_url=msg["source_url"]) except HomeAssistantError as err: connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) return try: await domain_blueprints[domain].async_add_blueprint(blueprint, path) except FileAlreadyExists: connection.send_error(msg["id"], "already_exists", "File already exists") return except OSError as err: connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) return connection.send_result( msg["id"], )
def test_input(): """Test loading inputs.""" data = {"hello": yaml.Input("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data
def test_placeholder(): """Test loading placeholders.""" data = {"hello": yaml.Placeholder("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data
async def async_update_alerts() -> None: nonlocal last_alerts active_alerts: dict[str, str | None] = {} for issue_id, alert in coordinator.data.items(): # Skip creation if already created and not updated since then if issue_id in last_alerts and alert.date_updated == last_alerts[ issue_id]: active_alerts[issue_id] = alert.date_updated continue # Fetch alert to get title + description try: response = await async_get_clientsession(hass).get( f"https://alerts.home-assistant.io/alerts/{alert.filename}", timeout=aiohttp.ClientTimeout(total=10), ) except asyncio.TimeoutError: _LOGGER.warning("Error fetching %s: timeout", alert.filename) continue alert_content = await response.text() alert_parts = alert_content.split("---") if len(alert_parts) != 3: _LOGGER.warning("Error parsing %s: unexpected metadata format", alert.filename) continue try: alert_info = parse_yaml(alert_parts[1]) except ValueError as err: _LOGGER.warning("Error parsing %s metadata: %s", alert.filename, err) continue if not isinstance(alert_info, dict) or "title" not in alert_info: _LOGGER.warning("Error in %s metadata: title not found", alert.filename) continue alert_title = alert_info["title"] alert_content = alert_parts[2].strip() async_create_issue( hass, DOMAIN, issue_id, is_fixable=False, issue_domain=alert.integration, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ "title": alert_title, "description": alert_content, }, ) active_alerts[issue_id] = alert.date_updated inactive_alerts = last_alerts.keys() - active_alerts.keys() for issue_id in inactive_alerts: async_delete_issue(hass, DOMAIN, issue_id) last_alerts = active_alerts
def test_input(try_both_loaders, try_both_dumpers): """Test loading inputs.""" data = {"hello": yaml.Input("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data