Example #1
0
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)
Example #2
0
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)
Example #3
0
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
    )
Example #4
0
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
Example #5
0
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)
Example #6
0
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"],
    )
Example #7
0
def test_input():
    """Test loading inputs."""
    data = {"hello": yaml.Input("test_name")}
    assert yaml.parse_yaml(yaml.dump(data)) == data
Example #8
0
def test_placeholder():
    """Test loading placeholders."""
    data = {"hello": yaml.Placeholder("test_name")}
    assert yaml.parse_yaml(yaml.dump(data)) == data
Example #9
0
    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
Example #10
0
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