<commit_before>"""
mbed SDK
Copyright (c) 2011-2013 ARM Limited

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
import inspect
import os
import argparse
import math
from os import listdir, remove, makedirs
from shutil import copyfile
from os.path import isdir, join, exists, split, relpath, splitext
from subprocess import Popen, PIPE, STDOUT, call
import json
from collections import OrderedDict

def cmd(l, check=True, verbose=False, shell=False, cwd=None):
    text = l if shell else ' '.join(l)
    if verbose:
        print text
    rc = call(l, shell=shell, cwd=cwd)
    if check and rc != 0:
        raise Exception('ERROR %d: "%s"' % (rc, text))

def run_cmd(command, wd=None, redirect=False):
    assert is_cmd_valid(command[0])
    try:
        p = Popen(command, stdout=PIPE, stderr=STDOUT if redirect else PIPE, cwd=wd)
        _stdout, _stderr = p.communicate()
    except OSError as e:
        print "[OS ERROR] Command: "+(' '.join(command))
        raise

    return _stdout, _stderr, p.returncode

def run_cmd_ext(command):
    assert is_cmd_valid(command[0])
    p = Popen(command, stdout=PIPE, stderr=PIPE)
    _stdout, _stderr = p.communicate()
    return _stdout, _stderr, p.returncode

def is_cmd_valid(cmd):
    caller = get_caller_name()
    abspath = find_cmd_abspath(cmd)
    if not abspath:
        error("%s: Command '%s' can't be found" % (caller, cmd))
    if not is_exec(abspath):
        error("%s: Command '%s' resolves to file '%s' which is not executable" % (caller, cmd, abspath))
    return True

def is_exec(path):
    return os.access(path, os.X_OK) or os.access(path+'.exe', os.X_OK)

def find_cmd_abspath(cmd):
    """ Returns the absolute path to a command.
        None is returned if no absolute path was found.
    """
    if exists(cmd) or exists(cmd + '.exe'):
        return os.path.abspath(cmd)
    if not 'PATH' in os.environ:
        raise Exception("Can't find command path for current platform ('%s')" % sys.platform)
    PATH=os.environ['PATH']
    for path in PATH.split(os.pathsep):
        abspath = '%s/%s' % (path, cmd)
        if exists(abspath) or exists(abspath + '.exe'):
            return abspath

def mkdir(path):
    if not exists(path):
        makedirs(path)

def copy_file(src, dst):
    """ Implement the behaviour of "shutil.copy(src, dst)" without copying the
        permissions (this was causing errors with directories mounted with samba)
    """
    if isdir(dst):
        _, file = split(src)
        dst = join(dst, file)
    copyfile(src, dst)

def delete_dir_files(dir):
    if not exists(dir):
        return

    for f in listdir(dir):
        file = join(dir, f)
        if not isdir(file):
            remove(file)

def get_caller_name(steps=2):
    """
    When called inside a function, it returns the name
    of the caller of that function.
    """
    return inspect.stack()[steps][3]

def error(msg):
    print("ERROR: %s" % msg)
    sys.exit(1)

def rel_path(path, base, dot=False):
    p = relpath(path, base)
    if dot and not p.startswith('.'):
        p = './' + p
    return p

class ToolException(Exception):
    pass

class NotSupportedException(Exception):
    pass

def split_path(path):
    base, file = split(path)
    name, ext = splitext(file)
    return base, name, ext

def args_error(parser, message):
    print "\n\n%s\n\n" % message
    parser.print_help()
    sys.exit()

def construct_enum(**enums):
    """ Create your own pseudo-enums """
    return type('Enum', (), enums)

def check_required_modules(required_modules, verbose=True):
    """ Function checks for Python modules which should be "importable" (installed)
        before test suite can be used.
        @return returns True if all modules are installed already
    """
    import imp
    not_installed_modules = []
    for module_name in required_modules:
        try:
            imp.find_module(module_name)
        except ImportError as e:
            # We also test against a rare case: module is an egg file
            try:
                __import__(module_name)
            except ImportError as e:
                not_installed_modules.append(module_name)
                if verbose:
                    print "Error: %s" % e

    if verbose:
        if not_installed_modules:
            print "Warning: Module(s) %s not installed. Please install required module(s) before using this script."% (', '.join(not_installed_modules))

    if not_installed_modules:
        return False
    else:
        return True

# Utility function: traverse a dictionary and change all the strings in the dictionary to
# ASCII from Unicode. Useful when reading ASCII JSON data, because the JSON decoder always
# returns Unicode string.
# Based on http://stackoverflow.com/a/13105359
def dict_to_ascii(input):
    if isinstance(input, dict):
        return OrderedDict([(dict_to_ascii(key), dict_to_ascii(value)) for key, value in input.iteritems()])
    elif isinstance(input, list):
        return [dict_to_ascii(element) for element in input]
    elif isinstance(input, unicode):
        return input.encode('ascii')
    else:
        return input

# Read a JSON file and return its Python representation, transforming all the strings from Unicode
# to ASCII. The order of keys in the JSON file is preserved.
def json_file_to_dict(fname):
    with open(fname, "rt") as f:
        return dict_to_ascii(json.load(f, object_pairs_hook=OrderedDict))

# Wowza, double closure
def argparse_type(casedness, prefer_hyphen=False) :
    def middle(list, type_name):
        # validate that an argument passed in (as string) is a member of the list of possible
        # arguments. Offer a suggestion if the case of the string, or the hyphens/underscores
        # do not match the expected style of the argument.
        def parse_type(string):
            if prefer_hyphen: newstring = casedness(string).replace("_","-")
            else:             newstring = casedness(string).replace("-","_")
            if string in list:
                return string
            elif string not in list and newstring in list:
                raise argparse.ArgumentTypeError("{0} is not a supported {1}. Did you mean {2}?".format(string, type_name, newstring))
            else:
                raise argparse.ArgumentTypeError("{0} is not a supported {1}. Supported {1}s are:\n{2}".format(string, type_name, columnate(list)))
        return parse_type
    return middle

# short cuts for the argparse_type versions
argparse_uppercase_type = argparse_type(str.upper, False)
argparse_lowercase_type = argparse_type(str.lower, False)
argparse_uppercase_hyphen_type = argparse_type(str.upper, True)
argparse_lowercase_hyphen_type = argparse_type(str.lower, True)

def argparse_force_type(case):
    def middle(list, type_name):
        # validate that an argument passed in (as string) is a member of the list of possible
        # arguments after converting it's case. Offer a suggestion if the hyphens/underscores
        # do not match the expected style of the argument.
        def parse_type(string):
            string = case(string)
            newstring = string.replace("-","_")
            if string in list:
                return string
            elif string not in list and newstring in list:
                raise argparse.ArgumentTypeError("{0} is not a supported {1}. Did you mean {2}?".format(string, type_name, newstring))
            else:
                raise argparse.ArgumentTypeError("{0} is not a supported {1}. Supported {1}s are:\n{2}".format(string, type_name, columnate(list)))
        return parse_type
    return middle

# these two types convert the case of their arguments _before_ validation
argparse_force_uppercase_type = argparse_force_type(str.upper)
argparse_force_lowercase_type = argparse_force_type(str.lower)

# An argument parser combinator that takes in an argument parser and creates a new parser that
# accepts a comma separated list of the same thing.
def argparse_many(fn):
    def wrap(string):
        return [fn(s) for s in string.split(",")]
    return wrap

# An argument parser that verifies that a string passed in corresponds to a file
def argparse_filestring_type(string) :
    if exists(string) :
        return string
    else :
        raise argparse.ArgumentTypeError("{0}"" does not exist in the filesystem.".format(string))

# render a list of strings as a in a bunch of columns
def columnate(strings, seperator=", ", chars=80):
    col_width = max(len(s) for s in strings)
    total_width = col_width + len(seperator)
    columns = math.floor(chars / total_width)
    output = ""
    for i, s in zip(range(len(strings)), strings):
        append = s
        if i != len(strings) - 1:
            append += seperator
        if i % columns == columns - 1:
            append += "\n"
        else:
            append = append.ljust(total_width)
        output += append
    return output
<commit_msg>Rework the force types to match case<commit_after>"""
mbed SDK
Copyright (c) 2011-2013 ARM Limited

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
import inspect
import os
import argparse
import math
from os import listdir, remove, makedirs
from shutil import copyfile
from os.path import isdir, join, exists, split, relpath, splitext
from subprocess import Popen, PIPE, STDOUT, call
import json
from collections import OrderedDict

def cmd(l, check=True, verbose=False, shell=False, cwd=None):
    text = l if shell else ' '.join(l)
    if verbose:
        print text
    rc = call(l, shell=shell, cwd=cwd)
    if check and rc != 0:
        raise Exception('ERROR %d: "%s"' % (rc, text))

def run_cmd(command, wd=None, redirect=False):
    assert is_cmd_valid(command[0])
    try:
        p = Popen(command, stdout=PIPE, stderr=STDOUT if redirect else PIPE, cwd=wd)
        _stdout, _stderr = p.communicate()
    except OSError as e:
        print "[OS ERROR] Command: "+(' '.join(command))
        raise

    return _stdout, _stderr, p.returncode

def run_cmd_ext(command):
    assert is_cmd_valid(command[0])
    p = Popen(command, stdout=PIPE, stderr=PIPE)
    _stdout, _stderr = p.communicate()
    return _stdout, _stderr, p.returncode

def is_cmd_valid(cmd):
    caller = get_caller_name()
    abspath = find_cmd_abspath(cmd)
    if not abspath:
        error("%s: Command '%s' can't be found" % (caller, cmd))
    if not is_exec(abspath):
        error("%s: Command '%s' resolves to file '%s' which is not executable" % (caller, cmd, abspath))
    return True

def is_exec(path):
    return os.access(path, os.X_OK) or os.access(path+'.exe', os.X_OK)

def find_cmd_abspath(cmd):
    """ Returns the absolute path to a command.
        None is returned if no absolute path was found.
    """
    if exists(cmd) or exists(cmd + '.exe'):
        return os.path.abspath(cmd)
    if not 'PATH' in os.environ:
        raise Exception("Can't find command path for current platform ('%s')" % sys.platform)
    PATH=os.environ['PATH']
    for path in PATH.split(os.pathsep):
        abspath = '%s/%s' % (path, cmd)
        if exists(abspath) or exists(abspath + '.exe'):
            return abspath

def mkdir(path):
    if not exists(path):
        makedirs(path)

def copy_file(src, dst):
    """ Implement the behaviour of "shutil.copy(src, dst)" without copying the
        permissions (this was causing errors with directories mounted with samba)
    """
    if isdir(dst):
        _, file = split(src)
        dst = join(dst, file)
    copyfile(src, dst)

def delete_dir_files(dir):
    if not exists(dir):
        return

    for f in listdir(dir):
        file = join(dir, f)
        if not isdir(file):
            remove(file)

def get_caller_name(steps=2):
    """
    When called inside a function, it returns the name
    of the caller of that function.
    """
    return inspect.stack()[steps][3]

def error(msg):
    print("ERROR: %s" % msg)
    sys.exit(1)

def rel_path(path, base, dot=False):
    p = relpath(path, base)
    if dot and not p.startswith('.'):
        p = './' + p
    return p

class ToolException(Exception):
    pass

class NotSupportedException(Exception):
    pass

def split_path(path):
    base, file = split(path)
    name, ext = splitext(file)
    return base, name, ext

def args_error(parser, message):
    print "\n\n%s\n\n" % message
    parser.print_help()
    sys.exit()

def construct_enum(**enums):
    """ Create your own pseudo-enums """
    return type('Enum', (), enums)

def check_required_modules(required_modules, verbose=True):
    """ Function checks for Python modules which should be "importable" (installed)
        before test suite can be used.
        @return returns True if all modules are installed already
    """
    import imp
    not_installed_modules = []
    for module_name in required_modules:
        try:
            imp.find_module(module_name)
        except ImportError as e:
            # We also test against a rare case: module is an egg file
            try:
                __import__(module_name)
            except ImportError as e:
                not_installed_modules.append(module_name)
                if verbose:
                    print "Error: %s" % e

    if verbose:
        if not_installed_modules:
            print "Warning: Module(s) %s not installed. Please install required module(s) before using this script."% (', '.join(not_installed_modules))

    if not_installed_modules:
        return False
    else:
        return True

# Utility function: traverse a dictionary and change all the strings in the dictionary to
# ASCII from Unicode. Useful when reading ASCII JSON data, because the JSON decoder always
# returns Unicode string.
# Based on http://stackoverflow.com/a/13105359
def dict_to_ascii(input):
    if isinstance(input, dict):
        return OrderedDict([(dict_to_ascii(key), dict_to_ascii(value)) for key, value in input.iteritems()])
    elif isinstance(input, list):
        return [dict_to_ascii(element) for element in input]
    elif isinstance(input, unicode):
        return input.encode('ascii')
    else:
        return input

# Read a JSON file and return its Python representation, transforming all the strings from Unicode
# to ASCII. The order of keys in the JSON file is preserved.
def json_file_to_dict(fname):
    with open(fname, "rt") as f:
        return dict_to_ascii(json.load(f, object_pairs_hook=OrderedDict))

# Wowza, double closure
def argparse_type(casedness, prefer_hyphen=False) :
    def middle(list, type_name):
        # validate that an argument passed in (as string) is a member of the list of possible
        # arguments. Offer a suggestion if the case of the string, or the hyphens/underscores
        # do not match the expected style of the argument.
        def parse_type(string):
            if prefer_hyphen: newstring = casedness(string).replace("_","-")
            else:             newstring = casedness(string).replace("-","_")
            if string in list:
                return string
            elif string not in list and newstring in list:
                raise argparse.ArgumentTypeError("{0} is not a supported {1}. Did you mean {2}?".format(string, type_name, newstring))
            else:
                raise argparse.ArgumentTypeError("{0} is not a supported {1}. Supported {1}s are:\n{2}".format(string, type_name, columnate(list)))
        return parse_type
    return middle

# short cuts for the argparse_type versions
argparse_uppercase_type = argparse_type(str.upper, False)
argparse_lowercase_type = argparse_type(str.lower, False)
argparse_uppercase_hyphen_type = argparse_type(str.upper, True)
argparse_lowercase_hyphen_type = argparse_type(str.lower, True)

def argparse_force_type(case):
    def middle(list, type_name):
        # validate that an argument passed in (as string) is a member of the list of possible
        # arguments after converting it's case. Offer a suggestion if the hyphens/underscores
        # do not match the expected style of the argument.
        def parse_type(string):
            for option in list:
                if case(string) == case(option):
                    return option
            raise argparse.ArgumentTypeError("{0} is not a supported {1}. Supported {1}s are:\n{2}".format(string, type_name, columnate(list)))
        return parse_type
    return middle

# these two types convert the case of their arguments _before_ validation
argparse_force_uppercase_type = argparse_force_type(str.upper)
argparse_force_lowercase_type = argparse_force_type(str.lower)

# An argument parser combinator that takes in an argument parser and creates a new parser that
# accepts a comma separated list of the same thing.
def argparse_many(fn):
    def wrap(string):
        return [fn(s) for s in string.split(",")]
    return wrap

# An argument parser that verifies that a string passed in corresponds to a file
def argparse_filestring_type(string) :
    if exists(string) :
        return string
    else :
        raise argparse.ArgumentTypeError("{0}"" does not exist in the filesystem.".format(string))

# render a list of strings as a in a bunch of columns
def columnate(strings, seperator=", ", chars=80):
    col_width = max(len(s) for s in strings)
    total_width = col_width + len(seperator)
    columns = math.floor(chars / total_width)
    output = ""
    for i, s in zip(range(len(strings)), strings):
        append = s
        if i != len(strings) - 1:
            append += seperator
        if i % columns == columns - 1:
            append += "\n"
        else:
            append = append.ljust(total_width)
        output += append
    return output
<|endoftext|>


<commit_before>from django.conf.urls import url
from django.contrib.auth import views as auth_views

from topics.views import sso
from .views import users
from .views import topics

urlpatterns = [
    # User authentication
    url(r'^_login/$', users.login_view, name='login'),
    url(r'^_login/sso$', sso.sso),
    url(r'^_accounts/login/$', users.login_view, name='login'),
    url(r'^_create_account/$', users.create_account_view, name='create_account'),
    url(r'^_logout/$', users.logout_view, name='logout'),
    url(r'^_reset/done/$', auth_views.password_reset_complete, name='password_reset_complete'),
    # Topics
    url(r'^_tag/$', topics.tag_topic, name='tag_topic'),
    url(r'^_icon/([0-9]+)\.ico$', topics.get_link_icon, name='get_link_icon'),
    url(r'^(.*)/$', topics.get_topic, name='get_topic'),
    url(r'^()$', topics.get_topic, name='get_topic'),
]
<commit_msg>upgrade url patterns<commit_after>from django.urls import re_path
from django.contrib.auth import views as auth_views

from topics.views import sso
from .views import users
from .views import topics

urlpatterns = [
    # User authentication
    re_path(r'^_login/$', users.login_view, name='login'),
    re_path(r'^_login/sso$', sso.sso),
    re_path(r'^_accounts/login/$', users.login_view, name='login'),
    re_path(r'^_create_account/$', users.create_account_view, name='create_account'),
    re_path(r'^_logout/$', users.logout_view, name='logout'),
    re_path(r'^_reset/done/$', auth_views.password_reset_complete, name='password_reset_complete'),
    # Topics
    re_path(r'^_tag/$', topics.tag_topic, name='tag_topic'),
    re_path(r'^_icon/([0-9]+)\.ico$', topics.get_link_icon, name='get_link_icon'),
    re_path(r'^(.*)/$', topics.get_topic, name='get_topic'),
    re_path(r'^()$', topics.get_topic, name='get_topic'),
]
<|endoftext|>


<commit_before>"""Config flow for DSMR integration."""
import asyncio
from functools import partial
import logging
from typing import Any, Dict, Optional

from async_timeout import timeout
from dsmr_parser import obis_references as obis_ref
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
import serial

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_PORT

from .const import (  # pylint:disable=unused-import
    CONF_DSMR_VERSION,
    CONF_SERIAL_ID,
    CONF_SERIAL_ID_GAS,
    DOMAIN,
)

_LOGGER = logging.getLogger(__name__)

class DSMRConnection:
    """Test the connection to DSMR and receive telegram to read serial ids."""

    def __init__(self, host, port, dsmr_version):
        """Initialize."""
        self._host = host
        self._port = port
        self._dsmr_version = dsmr_version
        self._telegram = {}

    def equipment_identifier(self):
        """Equipment identifier."""
        if obis_ref.EQUIPMENT_IDENTIFIER in self._telegram:
            dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER]
            return getattr(dsmr_object, "value", None)

    def equipment_identifier_gas(self):
        """Equipment identifier gas."""
        if obis_ref.EQUIPMENT_IDENTIFIER_GAS in self._telegram:
            dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER_GAS]
            return getattr(dsmr_object, "value", None)

    async def validate_connect(self, hass: core.HomeAssistant) -> bool:
        """Test if we can validate connection with the device."""

        def update_telegram(telegram):
            self._telegram = telegram

            transport.close()

        if self._host is None:
            reader_factory = partial(
                create_dsmr_reader,
                self._port,
                self._dsmr_version,
                update_telegram,
                loop=hass.loop,
            )
        else:
            reader_factory = partial(
                create_tcp_dsmr_reader,
                self._host,
                self._port,
                self._dsmr_version,
                update_telegram,
                loop=hass.loop,
            )

        try:
            transport, protocol = await asyncio.create_task(reader_factory())
        except (serial.serialutil.SerialException, OSError):
            _LOGGER.exception("Error connecting to DSMR")
            return False

        if transport:
            try:
                async with timeout(30):
                    await protocol.wait_closed()
            except asyncio.TimeoutError:
                # Timeout (no data received), close transport and return True (if telegram is empty, will result in CannotCommunicate error)
                transport.close()
                await protocol.wait_closed()
        return True

async def _validate_dsmr_connection(hass: core.HomeAssistant, data):
    """Validate the user input allows us to connect."""
    conn = DSMRConnection(data.get(CONF_HOST), data[CONF_PORT], data[CONF_DSMR_VERSION])

    if not await conn.validate_connect(hass):
        raise CannotConnect

    equipment_identifier = conn.equipment_identifier()
    equipment_identifier_gas = conn.equipment_identifier_gas()

    # Check only for equipment identifier in case no gas meter is connected
    if equipment_identifier is None:
        raise CannotCommunicate

    info = {
        CONF_SERIAL_ID: equipment_identifier,
        CONF_SERIAL_ID_GAS: equipment_identifier_gas,
    }

    return info

class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
    """Handle a config flow for DSMR."""

    VERSION = 1
    CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

    def _abort_if_host_port_configured(
        self,
        port: str,
        host: str = None,
        updates: Optional[Dict[Any, Any]] = None,
        reload_on_update: bool = True,
    ):
        """Test if host and port are already configured."""
        for entry in self._async_current_entries():
            if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port:
                if updates is not None:
                    changed = self.hass.config_entries.async_update_entry(
                        entry, data={**entry.data, **updates}
                    )
                    if (
                        changed
                        and reload_on_update
                        and entry.state
                        in (
                            config_entries.ENTRY_STATE_LOADED,
                            config_entries.ENTRY_STATE_SETUP_RETRY,
                        )
                    ):
                        self.hass.async_create_task(
                            self.hass.config_entries.async_reload(entry.entry_id)
                        )
                return self.async_abort(reason="already_configured")

        return None

    async def async_step_import(self, import_config=None):
        """Handle the initial step."""
        host = import_config.get(CONF_HOST)
        port = import_config[CONF_PORT]

        status = self._abort_if_host_port_configured(port, host, import_config)
        if status is not None:
            return status

        try:
            info = await _validate_dsmr_connection(self.hass, import_config)
        except CannotConnect:
            return self.async_abort(reason="cannot_connect")
        except CannotCommunicate:
            return self.async_abort(reason="cannot_communicate")

        if host is not None:
            name = f"{host}:{port}"
        else:
            name = port

        data = {**import_config, **info}

        await self.async_set_unique_id(info[CONF_SERIAL_ID])
        self._abort_if_unique_id_configured(data)

        return self.async_create_entry(title=name, data=data)

class CannotConnect(exceptions.HomeAssistantError):
    """Error to indicate we cannot connect."""

class CannotCommunicate(exceptions.HomeAssistantError):
    """Error to indicate we cannot connect."""
<commit_msg>Fix connection validation during import for dsmr integration (#40548)<commit_after>"""Config flow for DSMR integration."""
import asyncio
from functools import partial
import logging
from typing import Any, Dict, Optional

from async_timeout import timeout
from dsmr_parser import obis_references as obis_ref
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
import serial

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_PORT

from .const import (  # pylint:disable=unused-import
    CONF_DSMR_VERSION,
    CONF_SERIAL_ID,
    CONF_SERIAL_ID_GAS,
    DOMAIN,
)

_LOGGER = logging.getLogger(__name__)

class DSMRConnection:
    """Test the connection to DSMR and receive telegram to read serial ids."""

    def __init__(self, host, port, dsmr_version):
        """Initialize."""
        self._host = host
        self._port = port
        self._dsmr_version = dsmr_version
        self._telegram = {}

    def equipment_identifier(self):
        """Equipment identifier."""
        if obis_ref.EQUIPMENT_IDENTIFIER in self._telegram:
            dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER]
            return getattr(dsmr_object, "value", None)

    def equipment_identifier_gas(self):
        """Equipment identifier gas."""
        if obis_ref.EQUIPMENT_IDENTIFIER_GAS in self._telegram:
            dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER_GAS]
            return getattr(dsmr_object, "value", None)

    async def validate_connect(self, hass: core.HomeAssistant) -> bool:
        """Test if we can validate connection with the device."""

        def update_telegram(telegram):
            if obis_ref.EQUIPMENT_IDENTIFIER in telegram:
                self._telegram = telegram
                transport.close()

        if self._host is None:
            reader_factory = partial(
                create_dsmr_reader,
                self._port,
                self._dsmr_version,
                update_telegram,
                loop=hass.loop,
            )
        else:
            reader_factory = partial(
                create_tcp_dsmr_reader,
                self._host,
                self._port,
                self._dsmr_version,
                update_telegram,
                loop=hass.loop,
            )

        try:
            transport, protocol = await asyncio.create_task(reader_factory())
        except (serial.serialutil.SerialException, OSError):
            _LOGGER.exception("Error connecting to DSMR")
            return False

        if transport:
            try:
                async with timeout(30):
                    await protocol.wait_closed()
            except asyncio.TimeoutError:
                # Timeout (no data received), close transport and return True (if telegram is empty, will result in CannotCommunicate error)
                transport.close()
                await protocol.wait_closed()
        return True

async def _validate_dsmr_connection(hass: core.HomeAssistant, data):
    """Validate the user input allows us to connect."""
    conn = DSMRConnection(data.get(CONF_HOST), data[CONF_PORT], data[CONF_DSMR_VERSION])

    if not await conn.validate_connect(hass):
        raise CannotConnect

    equipment_identifier = conn.equipment_identifier()
    equipment_identifier_gas = conn.equipment_identifier_gas()

    # Check only for equipment identifier in case no gas meter is connected
    if equipment_identifier is None:
        raise CannotCommunicate

    info = {
        CONF_SERIAL_ID: equipment_identifier,
        CONF_SERIAL_ID_GAS: equipment_identifier_gas,
    }

    return info

class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
    """Handle a config flow for DSMR."""

    VERSION = 1
    CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

    def _abort_if_host_port_configured(
        self,
        port: str,
        host: str = None,
        updates: Optional[Dict[Any, Any]] = None,
        reload_on_update: bool = True,
    ):
        """Test if host and port are already configured."""
        for entry in self._async_current_entries():
            if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port:
                if updates is not None:
                    changed = self.hass.config_entries.async_update_entry(
                        entry, data={**entry.data, **updates}
                    )
                    if (
                        changed
                        and reload_on_update
                        and entry.state
                        in (
                            config_entries.ENTRY_STATE_LOADED,
                            config_entries.ENTRY_STATE_SETUP_RETRY,
                        )
                    ):
                        self.hass.async_create_task(
                            self.hass.config_entries.async_reload(entry.entry_id)
                        )
                return self.async_abort(reason="already_configured")

        return None

    async def async_step_import(self, import_config=None):
        """Handle the initial step."""
        host = import_config.get(CONF_HOST)
        port = import_config[CONF_PORT]

        status = self._abort_if_host_port_configured(port, host, import_config)
        if status is not None:
            return status

        try:
            info = await _validate_dsmr_connection(self.hass, import_config)
        except CannotConnect:
            return self.async_abort(reason="cannot_connect")
        except CannotCommunicate:
            return self.async_abort(reason="cannot_communicate")

        if host is not None:
            name = f"{host}:{port}"
        else:
            name = port

        data = {**import_config, **info}

        await self.async_set_unique_id(info[CONF_SERIAL_ID])
        self._abort_if_unique_id_configured(data)

        return self.async_create_entry(title=name, data=data)

class CannotConnect(exceptions.HomeAssistantError):
    """Error to indicate we cannot connect."""

class CannotCommunicate(exceptions.HomeAssistantError):
    """Error to indicate we cannot connect."""
<|endoftext|>


<commit_before>"""Support for Honeywell (US) Total Connect Comfort sensors."""
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from somecomfort import Device

from homeassistant.components.sensor import (
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import (
    DEVICE_CLASS_HUMIDITY,
    DEVICE_CLASS_TEMPERATURE,
    PERCENTAGE,
    TEMP_CELSIUS,
    TEMP_FAHRENHEIT,
)
from homeassistant.helpers.typing import StateType

from .const import DOMAIN, HUMIDITY_STATUS_KEY, TEMPERATURE_STATUS_KEY

def _get_temperature_sensor_unit(device: Device) -> str:
    """Get the correct temperature unit for the device."""
    return TEMP_CELSIUS if device.temperature_unit == "C" else TEMP_FAHRENHEIT

@dataclass
class HoneywellSensorEntityDescriptionMixin:
    """Mixin for required keys."""

    value_fn: Callable[[Device], Any]
    unit_fn: Callable[[Device], Any]

@dataclass
class HoneywellSensorEntityDescription(
    SensorEntityDescription, HoneywellSensorEntityDescriptionMixin
):
    """Describes a Honeywell sensor entity."""

SENSOR_TYPES: tuple[HoneywellSensorEntityDescription, ...] = (
    HoneywellSensorEntityDescription(
        key=TEMPERATURE_STATUS_KEY,
        name="Temperature",
        device_class=DEVICE_CLASS_TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        value_fn=lambda device: device.outdoor_temperature,
        unit_fn=_get_temperature_sensor_unit,
    ),
    HoneywellSensorEntityDescription(
        key=HUMIDITY_STATUS_KEY,
        name="Humidity",
        device_class=DEVICE_CLASS_HUMIDITY,
        state_class=SensorStateClass.MEASUREMENT,
        value_fn=lambda device: device.outdoor_humidity,
        unit_fn=lambda device: PERCENTAGE,
    ),
)

async def async_setup_entry(hass, config, async_add_entities, discovery_info=None):
    """Set up the Honeywell thermostat."""
    data = hass.data[DOMAIN][config.entry_id]
    sensors = []

    for device in data.devices.values():
        for description in SENSOR_TYPES:
            if getattr(device, description.key) is not None:
                sensors.append(HoneywellSensor(device, description))

    async_add_entities(sensors)

class HoneywellSensor(SensorEntity):
    """Representation of a Honeywell US Outdoor Temperature Sensor."""

    entity_description: HoneywellSensorEntityDescription

    def __init__(self, device, description):
        """Initialize the outdoor temperature sensor."""
        self._device = device
        self.entity_description = description
        self._attr_unique_id = f"{device.deviceid}_outdoor_{description.device_class}"
        self._attr_name = f"{device.name} outdoor {description.device_class}"
        self._attr_native_unit_of_measurement = description.unit_fn(device)

    @property
    def native_value(self) -> StateType:
        """Return the state."""
        return self.entity_description.value_fn(self._device)
<commit_msg>Use SensorDeviceClass in honeywell (#69298)<commit_after>"""Support for Honeywell (US) Total Connect Comfort sensors."""
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from somecomfort import Device

from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.typing import StateType

from .const import DOMAIN, HUMIDITY_STATUS_KEY, TEMPERATURE_STATUS_KEY

def _get_temperature_sensor_unit(device: Device) -> str:
    """Get the correct temperature unit for the device."""
    return TEMP_CELSIUS if device.temperature_unit == "C" else TEMP_FAHRENHEIT

@dataclass
class HoneywellSensorEntityDescriptionMixin:
    """Mixin for required keys."""

    value_fn: Callable[[Device], Any]
    unit_fn: Callable[[Device], Any]

@dataclass
class HoneywellSensorEntityDescription(
    SensorEntityDescription, HoneywellSensorEntityDescriptionMixin
):
    """Describes a Honeywell sensor entity."""

SENSOR_TYPES: tuple[HoneywellSensorEntityDescription, ...] = (
    HoneywellSensorEntityDescription(
        key=TEMPERATURE_STATUS_KEY,
        name="Temperature",
        device_class=SensorDeviceClass.TEMPERATURE,
        state_class=SensorStateClass.MEASUREMENT,
        value_fn=lambda device: device.outdoor_temperature,
        unit_fn=_get_temperature_sensor_unit,
    ),
    HoneywellSensorEntityDescription(
        key=HUMIDITY_STATUS_KEY,
        name="Humidity",
        device_class=SensorDeviceClass.HUMIDITY,
        state_class=SensorStateClass.MEASUREMENT,
        value_fn=lambda device: device.outdoor_humidity,
        unit_fn=lambda device: PERCENTAGE,
    ),
)

async def async_setup_entry(hass, config, async_add_entities, discovery_info=None):
    """Set up the Honeywell thermostat."""
    data = hass.data[DOMAIN][config.entry_id]
    sensors = []

    for device in data.devices.values():
        for description in SENSOR_TYPES:
            if getattr(device, description.key) is not None:
                sensors.append(HoneywellSensor(device, description))

    async_add_entities(sensors)

class HoneywellSensor(SensorEntity):
    """Representation of a Honeywell US Outdoor Temperature Sensor."""

    entity_description: HoneywellSensorEntityDescription

    def __init__(self, device, description):
        """Initialize the outdoor temperature sensor."""
        self._device = device
        self.entity_description = description
        self._attr_unique_id = f"{device.deviceid}_outdoor_{description.device_class}"
        self._attr_name = f"{device.name} outdoor {description.device_class}"
        self._attr_native_unit_of_measurement = description.unit_fn(device)

    @property
    def native_value(self) -> StateType:
        """Return the state."""
        return self.entity_description.value_fn(self._device)
<|endoftext|>


<commit_before>"""Support for UK Met Office weather service."""
from __future__ import annotations

from typing import NamedTuple

from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
    ATTR_ATTRIBUTION,
    DEVICE_CLASS_HUMIDITY,
    DEVICE_CLASS_TEMPERATURE,
    LENGTH_KILOMETERS,
    PERCENTAGE,
    SPEED_MILES_PER_HOUR,
    TEMP_CELSIUS,
    UV_INDEX,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import (
    ATTRIBUTION,
    CONDITION_CLASSES,
    DOMAIN,
    METOFFICE_COORDINATES,
    METOFFICE_DAILY_COORDINATOR,
    METOFFICE_HOURLY_COORDINATOR,
    METOFFICE_NAME,
    MODE_3HOURLY_LABEL,
    MODE_DAILY,
    MODE_DAILY_LABEL,
    VISIBILITY_CLASSES,
    VISIBILITY_DISTANCE_CLASSES,
)

ATTR_LAST_UPDATE = "last_update"
ATTR_SENSOR_ID = "sensor_id"
ATTR_SITE_ID = "site_id"
ATTR_SITE_NAME = "site_name"

class MetOfficeSensorMetadata(NamedTuple):
    """Sensor metadata for an individual NWS sensor."""

    title: str
    device_class: str | None
    unit_of_measurement: str | None
    icon: str | None
    enabled_by_default: bool

SENSOR_TYPES = {
    "name": MetOfficeSensorMetadata(
        "Station Name",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:label-outline",
        enabled_by_default=False,
    ),
    "weather": MetOfficeSensorMetadata(
        "Weather",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:weather-sunny",  # but will adapt to current conditions
        enabled_by_default=True,
    ),
    "temperature": MetOfficeSensorMetadata(
        "Temperature",
        device_class=DEVICE_CLASS_TEMPERATURE,
        unit_of_measurement=TEMP_CELSIUS,
        icon=None,
        enabled_by_default=True,
    ),
    "feels_like_temperature": MetOfficeSensorMetadata(
        "Feels Like Temperature",
        device_class=DEVICE_CLASS_TEMPERATURE,
        unit_of_measurement=TEMP_CELSIUS,
        icon=None,
        enabled_by_default=False,
    ),
    "wind_speed": MetOfficeSensorMetadata(
        "Wind Speed",
        device_class=None,
        unit_of_measurement=SPEED_MILES_PER_HOUR,
        icon="mdi:weather-windy",
        enabled_by_default=True,
    ),
    "wind_direction": MetOfficeSensorMetadata(
        "Wind Direction",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:compass-outline",
        enabled_by_default=False,
    ),
    "wind_gust": MetOfficeSensorMetadata(
        "Wind Gust",
        device_class=None,
        unit_of_measurement=SPEED_MILES_PER_HOUR,
        icon="mdi:weather-windy",
        enabled_by_default=False,
    ),
    "visibility": MetOfficeSensorMetadata(
        "Visibility",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:eye",
        enabled_by_default=False,
    ),
    "visibility_distance": MetOfficeSensorMetadata(
        "Visibility Distance",
        device_class=None,
        unit_of_measurement=LENGTH_KILOMETERS,
        icon="mdi:eye",
        enabled_by_default=False,
    ),
    "uv": MetOfficeSensorMetadata(
        "UV Index",
        device_class=None,
        unit_of_measurement=UV_INDEX,
        icon="mdi:weather-sunny-alert",
        enabled_by_default=True,
    ),
    "precipitation": MetOfficeSensorMetadata(
        "Probability of Precipitation",
        device_class=None,
        unit_of_measurement=PERCENTAGE,
        icon="mdi:weather-rainy",
        enabled_by_default=True,
    ),
    "humidity": MetOfficeSensorMetadata(
        "Humidity",
        device_class=DEVICE_CLASS_HUMIDITY,
        unit_of_measurement=PERCENTAGE,
        icon=None,
        enabled_by_default=False,
    ),
}

async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigType, async_add_entities
) -> None:
    """Set up the Met Office weather sensor platform."""
    hass_data = hass.data[DOMAIN][entry.entry_id]

    async_add_entities(
        [
            MetOfficeCurrentSensor(
                hass_data[METOFFICE_HOURLY_COORDINATOR],
                hass_data,
                True,
                sensor_type,
                metadata,
            )
            for sensor_type, metadata in SENSOR_TYPES.items()
        ]
        + [
            MetOfficeCurrentSensor(
                hass_data[METOFFICE_DAILY_COORDINATOR],
                hass_data,
                False,
                sensor_type,
                metadata,
            )
            for sensor_type, metadata in SENSOR_TYPES.items()
        ],
        False,
    )

class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity):
    """Implementation of a Met Office current weather condition sensor."""

    def __init__(
        self,
        coordinator,
        hass_data,
        use_3hourly,
        sensor_type,
        metadata: MetOfficeSensorMetadata,
    ):
        """Initialize the sensor."""
        super().__init__(coordinator)

        self._type = sensor_type
        self._metadata = metadata
        mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL
        self._attr_name = f"{hass_data[METOFFICE_NAME]} {metadata.title} {mode_label}"
        self._attr_unique_id = f"{metadata.title}_{hass_data[METOFFICE_COORDINATES]}"
        if not use_3hourly:
            self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}"
        self._attr_device_class = metadata.device_class
        self._attr_unit_of_measurement = metadata.unit_of_measurement

        self.use_3hourly = use_3hourly

    @property
    def state(self):
        """Return the state of the sensor."""
        value = None

        if self._type == "visibility_distance" and hasattr(
            self.coordinator.data.now, "visibility"
        ):
            value = VISIBILITY_DISTANCE_CLASSES.get(
                self.coordinator.data.now.visibility.value
            )

        if self._type == "visibility" and hasattr(
            self.coordinator.data.now, "visibility"
        ):
            value = VISIBILITY_CLASSES.get(self.coordinator.data.now.visibility.value)

        elif self._type == "weather" and hasattr(self.coordinator.data.now, self._type):
            value = [
                k
                for k, v in CONDITION_CLASSES.items()
                if self.coordinator.data.now.weather.value in v
            ][0]

        elif hasattr(self.coordinator.data.now, self._type):
            value = getattr(self.coordinator.data.now, self._type)

            if not isinstance(value, int):
                value = value.value

        return value

    @property
    def icon(self):
        """Return the icon for the entity card."""
        value = self._metadata.icon
        if self._type == "weather":
            value = self.state
            if value is None:
                value = "sunny"
            elif value == "partlycloudy":
                value = "partly-cloudy"
            value = f"mdi:weather-{value}"

        return value

    @property
    def extra_state_attributes(self):
        """Return the state attributes of the device."""
        return {
            ATTR_ATTRIBUTION: ATTRIBUTION,
            ATTR_LAST_UPDATE: self.coordinator.data.now.date,
            ATTR_SENSOR_ID: self._type,
            ATTR_SITE_ID: self.coordinator.data.site.id,
            ATTR_SITE_NAME: self.coordinator.data.site.name,
        }

    @property
    def entity_registry_enabled_default(self) -> bool:
        """Return if the entity should be enabled when first added to the entity registry."""
        return self._metadata.enabled_by_default and self.use_3hourly
<commit_msg>Fix for issue #53031 (#53343)<commit_after>"""Support for UK Met Office weather service."""
from __future__ import annotations

from typing import NamedTuple

from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
    ATTR_ATTRIBUTION,
    DEVICE_CLASS_HUMIDITY,
    DEVICE_CLASS_TEMPERATURE,
    LENGTH_KILOMETERS,
    PERCENTAGE,
    SPEED_MILES_PER_HOUR,
    TEMP_CELSIUS,
    UV_INDEX,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import (
    ATTRIBUTION,
    CONDITION_CLASSES,
    DOMAIN,
    METOFFICE_COORDINATES,
    METOFFICE_DAILY_COORDINATOR,
    METOFFICE_HOURLY_COORDINATOR,
    METOFFICE_NAME,
    MODE_3HOURLY_LABEL,
    MODE_DAILY,
    MODE_DAILY_LABEL,
    VISIBILITY_CLASSES,
    VISIBILITY_DISTANCE_CLASSES,
)

ATTR_LAST_UPDATE = "last_update"
ATTR_SENSOR_ID = "sensor_id"
ATTR_SITE_ID = "site_id"
ATTR_SITE_NAME = "site_name"

class MetOfficeSensorMetadata(NamedTuple):
    """Sensor metadata for an individual NWS sensor."""

    title: str
    device_class: str | None
    unit_of_measurement: str | None
    icon: str | None
    enabled_by_default: bool

SENSOR_TYPES = {
    "name": MetOfficeSensorMetadata(
        "Station Name",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:label-outline",
        enabled_by_default=False,
    ),
    "weather": MetOfficeSensorMetadata(
        "Weather",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:weather-sunny",  # but will adapt to current conditions
        enabled_by_default=True,
    ),
    "temperature": MetOfficeSensorMetadata(
        "Temperature",
        device_class=DEVICE_CLASS_TEMPERATURE,
        unit_of_measurement=TEMP_CELSIUS,
        icon=None,
        enabled_by_default=True,
    ),
    "feels_like_temperature": MetOfficeSensorMetadata(
        "Feels Like Temperature",
        device_class=DEVICE_CLASS_TEMPERATURE,
        unit_of_measurement=TEMP_CELSIUS,
        icon=None,
        enabled_by_default=False,
    ),
    "wind_speed": MetOfficeSensorMetadata(
        "Wind Speed",
        device_class=None,
        unit_of_measurement=SPEED_MILES_PER_HOUR,
        icon="mdi:weather-windy",
        enabled_by_default=True,
    ),
    "wind_direction": MetOfficeSensorMetadata(
        "Wind Direction",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:compass-outline",
        enabled_by_default=False,
    ),
    "wind_gust": MetOfficeSensorMetadata(
        "Wind Gust",
        device_class=None,
        unit_of_measurement=SPEED_MILES_PER_HOUR,
        icon="mdi:weather-windy",
        enabled_by_default=False,
    ),
    "visibility": MetOfficeSensorMetadata(
        "Visibility",
        device_class=None,
        unit_of_measurement=None,
        icon="mdi:eye",
        enabled_by_default=False,
    ),
    "visibility_distance": MetOfficeSensorMetadata(
        "Visibility Distance",
        device_class=None,
        unit_of_measurement=LENGTH_KILOMETERS,
        icon="mdi:eye",
        enabled_by_default=False,
    ),
    "uv": MetOfficeSensorMetadata(
        "UV Index",
        device_class=None,
        unit_of_measurement=UV_INDEX,
        icon="mdi:weather-sunny-alert",
        enabled_by_default=True,
    ),
    "precipitation": MetOfficeSensorMetadata(
        "Probability of Precipitation",
        device_class=None,
        unit_of_measurement=PERCENTAGE,
        icon="mdi:weather-rainy",
        enabled_by_default=True,
    ),
    "humidity": MetOfficeSensorMetadata(
        "Humidity",
        device_class=DEVICE_CLASS_HUMIDITY,
        unit_of_measurement=PERCENTAGE,
        icon=None,
        enabled_by_default=False,
    ),
}

async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigType, async_add_entities
) -> None:
    """Set up the Met Office weather sensor platform."""
    hass_data = hass.data[DOMAIN][entry.entry_id]

    async_add_entities(
        [
            MetOfficeCurrentSensor(
                hass_data[METOFFICE_HOURLY_COORDINATOR],
                hass_data,
                True,
                sensor_type,
                metadata,
            )
            for sensor_type, metadata in SENSOR_TYPES.items()
        ]
        + [
            MetOfficeCurrentSensor(
                hass_data[METOFFICE_DAILY_COORDINATOR],
                hass_data,
                False,
                sensor_type,
                metadata,
            )
            for sensor_type, metadata in SENSOR_TYPES.items()
        ],
        False,
    )

class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity):
    """Implementation of a Met Office current weather condition sensor."""

    def __init__(
        self,
        coordinator,
        hass_data,
        use_3hourly,
        sensor_type,
        metadata: MetOfficeSensorMetadata,
    ):
        """Initialize the sensor."""
        super().__init__(coordinator)

        self._type = sensor_type
        self._metadata = metadata
        mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL
        self._attr_name = f"{hass_data[METOFFICE_NAME]} {metadata.title} {mode_label}"
        self._attr_unique_id = f"{metadata.title}_{hass_data[METOFFICE_COORDINATES]}"
        if not use_3hourly:
            self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}"
        self._attr_device_class = metadata.device_class
        self._attr_unit_of_measurement = metadata.unit_of_measurement

        self.use_3hourly = use_3hourly

    @property
    def state(self):
        """Return the state of the sensor."""
        value = None

        if self._type == "visibility_distance" and hasattr(
            self.coordinator.data.now, "visibility"
        ):
            value = VISIBILITY_DISTANCE_CLASSES.get(
                self.coordinator.data.now.visibility.value
            )

        if self._type == "visibility" and hasattr(
            self.coordinator.data.now, "visibility"
        ):
            value = VISIBILITY_CLASSES.get(self.coordinator.data.now.visibility.value)

        elif self._type == "weather" and hasattr(self.coordinator.data.now, self._type):
            value = [
                k
                for k, v in CONDITION_CLASSES.items()
                if self.coordinator.data.now.weather.value in v
            ][0]

        elif hasattr(self.coordinator.data.now, self._type):
            value = getattr(self.coordinator.data.now, self._type)

            if hasattr(value, "value"):
                value = value.value

        return value

    @property
    def icon(self):
        """Return the icon for the entity card."""
        value = self._metadata.icon
        if self._type == "weather":
            value = self.state
            if value is None:
                value = "sunny"
            elif value == "partlycloudy":
                value = "partly-cloudy"
            value = f"mdi:weather-{value}"

        return value

    @property
    def extra_state_attributes(self):
        """Return the state attributes of the device."""
        return {
            ATTR_ATTRIBUTION: ATTRIBUTION,
            ATTR_LAST_UPDATE: self.coordinator.data.now.date,
            ATTR_SENSOR_ID: self._type,
            ATTR_SITE_ID: self.coordinator.data.site.id,
            ATTR_SITE_NAME: self.coordinator.data.site.name,
        }

    @property
    def entity_registry_enabled_default(self) -> bool:
        """Return if the entity should be enabled when first added to the entity registry."""
        return self._metadata.enabled_by_default and self.use_3hourly
<|endoftext|>


<commit_before>"""
homeassistant.components.sensor.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Shows sensor values from Tellstick sensors.

Possible config keys:

id of the sensor: Name the sensor with ID
135=Outside

only_named: Only show the named sensors
only_named=1

temperature_scale: The scale of the temperature value
temperature_scale=°C

datatype_mask: mask to determine which sensor values to show based on
https://tellcore-py.readthedocs.org
    /en/v1.0.4/constants.html#module-tellcore.constants

datatype_mask=1   # only show temperature
datatype_mask=12  # only show rain rate and rain total
datatype_mask=127 # show all sensor values
"""
import logging
from collections import namedtuple

import tellcore.telldus as telldus
import tellcore.constants as tellcore_constants

from homeassistant.const import TEMP_CELCIUS
from homeassistant.helpers.entity import Entity
import homeassistant.util as util

DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])

REQUIREMENTS = ['tellcore-py==1.1.2']

# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
    """ Sets up Tellstick sensors. """
    sensor_value_descriptions = {
        tellcore_constants.TELLSTICK_TEMPERATURE:
        DatatypeDescription(
            'temperature', config.get('temperature_scale', TEMP_CELCIUS)),

        tellcore_constants.TELLSTICK_HUMIDITY:
        DatatypeDescription('humidity', '%'),

        tellcore_constants.TELLSTICK_RAINRATE:
        DatatypeDescription('rain rate', ''),

        tellcore_constants.TELLSTICK_RAINTOTAL:
        DatatypeDescription('rain total', ''),

        tellcore_constants.TELLSTICK_WINDDIRECTION:
        DatatypeDescription('wind direction', ''),

        tellcore_constants.TELLSTICK_WINDAVERAGE:
        DatatypeDescription('wind average', ''),

        tellcore_constants.TELLSTICK_WINDGUST:
        DatatypeDescription('wind gust', '')
    }

    try:
        core = telldus.TelldusCore()
    except OSError:
        logging.getLogger(__name__).exception(
            'Could not initialize Tellstick.')
        return

    sensors = []
    datatype_mask = util.convert(config.get('datatype_mask'), int, 127)

    for ts_sensor in core.sensors():
        try:
            sensor_name = config[ts_sensor.id]
        except KeyError:
            if util.convert(config.get('only_named'), bool, False):
                continue
            sensor_name = str(ts_sensor.id)

        for datatype in sensor_value_descriptions.keys():
            if datatype & datatype_mask and ts_sensor.has_value(datatype):

                sensor_info = sensor_value_descriptions[datatype]

                sensors.append(
                    TellstickSensor(
                        sensor_name, ts_sensor, datatype, sensor_info))

    add_devices(sensors)

class TellstickSensor(Entity):
    """ Represents a Tellstick sensor. """

    def __init__(self, name, sensor, datatype, sensor_info):
        self.datatype = datatype
        self.sensor = sensor
        self._unit_of_measurement = sensor_info.unit or None

        self._name = "{} {}".format(name, sensor_info.name)

    @property
    def name(self):
        """ Returns the name of the device. """
        return self._name

    @property
    def state(self):
        """ Returns the state of the device. """
        return self.sensor.value(self.datatype).value

    @property
    def unit_of_measurement(self):
        return self._unit_of_measurement
<commit_msg>Add link to docs<commit_after>"""
homeassistant.components.sensor.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Shows sensor values from Tellstick sensors.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.tellstick.html
"""
import logging
from collections import namedtuple

import tellcore.telldus as telldus
import tellcore.constants as tellcore_constants

from homeassistant.const import TEMP_CELCIUS
from homeassistant.helpers.entity import Entity
import homeassistant.util as util

DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit'])

REQUIREMENTS = ['tellcore-py==1.1.2']

# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
    """ Sets up Tellstick sensors. """
    sensor_value_descriptions = {
        tellcore_constants.TELLSTICK_TEMPERATURE:
        DatatypeDescription(
            'temperature', config.get('temperature_scale', TEMP_CELCIUS)),

        tellcore_constants.TELLSTICK_HUMIDITY:
        DatatypeDescription('humidity', '%'),

        tellcore_constants.TELLSTICK_RAINRATE:
        DatatypeDescription('rain rate', ''),

        tellcore_constants.TELLSTICK_RAINTOTAL:
        DatatypeDescription('rain total', ''),

        tellcore_constants.TELLSTICK_WINDDIRECTION:
        DatatypeDescription('wind direction', ''),

        tellcore_constants.TELLSTICK_WINDAVERAGE:
        DatatypeDescription('wind average', ''),

        tellcore_constants.TELLSTICK_WINDGUST:
        DatatypeDescription('wind gust', '')
    }

    try:
        core = telldus.TelldusCore()
    except OSError:
        logging.getLogger(__name__).exception(
            'Could not initialize Tellstick.')
        return

    sensors = []
    datatype_mask = util.convert(config.get('datatype_mask'), int, 127)

    for ts_sensor in core.sensors():
        try:
            sensor_name = config[ts_sensor.id]
        except KeyError:
            if util.convert(config.get('only_named'), bool, False):
                continue
            sensor_name = str(ts_sensor.id)

        for datatype in sensor_value_descriptions.keys():
            if datatype & datatype_mask and ts_sensor.has_value(datatype):

                sensor_info = sensor_value_descriptions[datatype]

                sensors.append(
                    TellstickSensor(
                        sensor_name, ts_sensor, datatype, sensor_info))

    add_devices(sensors)

class TellstickSensor(Entity):
    """ Represents a Tellstick sensor. """

    def __init__(self, name, sensor, datatype, sensor_info):
        self.datatype = datatype
        self.sensor = sensor
        self._unit_of_measurement = sensor_info.unit or None

        self._name = "{} {}".format(name, sensor_info.name)

    @property
    def name(self):
        """ Returns the name of the device. """
        return self._name

    @property
    def state(self):
        """ Returns the state of the device. """
        return self.sensor.value(self.datatype).value

    @property
    def unit_of_measurement(self):
        return self._unit_of_measurement
<|endoftext|>


<commit_before># Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Generates color tables to provide support for a wide range of browsers.

Requires input font have vector color capability, that is either an SVG table
or COLR/CPAL.

https://www.youtube.com/watch?v=HLddvNiXym4

Use cbdt for bitmaps because sbix is less x-platform than you'd guess
(https://github.com/harfbuzz/harfbuzz/issues/2679)

Sample usage:

maximum_color MySvgFont.ttf"""
from absl import app
from absl import flags
from absl import logging
from fontTools import ttLib
from fontTools.ttLib.ttFont import newTable
from nanoemoji import config
from nanoemoji.colr_to_svg import colr_glyphs
from nanoemoji.extract_svgs import svg_glyphs
from nanoemoji.ninja import (
    build_dir,
    gen_ninja,
    maybe_run_ninja,
    module_rule,
    rel_build,
    NinjaWriter,
)
from nanoemoji.util import only
from pathlib import Path
from typing import List, NamedTuple, Tuple

FLAGS = flags.FLAGS

flags.DEFINE_bool(
    "destroy_non_color_glyphs",
    True,
    "If true feel free to obliterate any existing glyf/cff content, e.g. fallback glyphs",
)
flags.DEFINE_bool(
    "bitmaps",
    False,
    "If true, generate a bitmap table (specificaly CBDT)",
)

# attribute names need to match inputs to write_font rule
class WriteFontInputs(NamedTuple):
    glyphmap_file: Path
    config_file: Path
    part_file: Path

    @property
    def table_tag(self) -> str:
        return f"{Path(self.glyphmap_file).stem:4}"

    @property
    def color_format(self) -> str:
        identifier = self.table_tag.strip().lower()

        if identifier == "svg":
            # for good woff2 performance, at cost of inflated size
            return "picosvg"
        elif identifier == "colr":
            # optimize for woff2 performance
            return "glyf_colr_1"
        elif identifier == "cbdt":
            return "cbdt"
        else:
            raise ValueError(f"What is {identifier}?!")

    @classmethod
    def for_tag(cls, table_tag: str) -> "WriteFontInputs":
        basename = table_tag.strip()
        return cls(
            Path(basename + ".glyphmap"),
            Path(basename + ".toml"),
            master_part_file_dest(),
        )

def _vector_color_table(font: ttLib.TTFont) -> str:
    has_svg = "SVG " in font
    has_colr = "COLR" in font
    if has_svg == has_colr:
        raise ValueError("Must have exactly one of COLR, SVG")

    if has_svg:
        return "SVG "
    if has_colr:
        return "COLR"

    raise AssertionError("Impossible")

def svg_extract_dir() -> Path:
    return build_dir() / "svg_dump"

def svg_generate_dir() -> Path:
    return build_dir() / "svg_generate"

def picosvg_dir() -> Path:
    return build_dir() / "picosvg"

def picosvg_dest(input_svg: Path) -> Path:
    return picosvg_dir() / input_svg.name

def part_file_dest(picosvg_file: Path) -> Path:
    return picosvg_file.with_suffix(".parts.json")

def master_part_file_dest() -> Path:
    return Path("parts-merged.json")

def bitmap_dir() -> Path:
    return build_dir() / "bitmap"

def bitmap_dest(input_svg: Path) -> Path:
    return bitmap_dir() / input_svg.with_suffix(".png").name

def _write_preamble(nw: NinjaWriter):
    module_rule(
        nw,
        "extract_svgs_from_otsvg",
        f"--output_dir {rel_build(svg_extract_dir())} $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "generate_svgs_from_colr",
        f"--output_dir {rel_build(svg_generate_dir())} $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "write_glyphmap_for_glyph_svgs",
        f"--output_file $out @$out.rsp",
        rspfile="$out.rsp",
        rspfile_content="$in",
    )
    nw.newline()

    module_rule(
        nw,
        "write_config_for_mergeable",
        "--color_format $color_format $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "write_font",
        f"--config_file $config_file --glyphmap_file $glyphmap_file  --part_file $part_file --output_file $out",
    )
    nw.newline()

    nw.rule(
        f"picosvg",
        f"picosvg --output_file $out $in",
    )
    nw.newline()

    module_rule(
        nw,
        "write_part_file",
        f"--reuse_tolerance $reuse_tolerance --wh $wh --output_file $out $in",
    )
    nw.newline()

    module_rule(
        nw,
        "write_combined_part_files",
        f"--output_file $out  @$out.rsp",
        rspfile="$out.rsp",
        rspfile_content="$in",
    )
    nw.newline()

    # set height only, let width scale proportionally
    res = config.load().bitmap_resolution
    nw.rule(
        "write_bitmap",
        f"resvg -h {res} $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "glue_together",
        f"--color_table $color_table --target_font $target_font --donor_font $in --output_file $out",
    )
    nw.newline()

    module_rule(
        nw,
        "keep_glyph_names",
        f"$in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "strip_glyph_names",
        f"$in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "copy",
        f"$in $out",
    )
    nw.newline()

def _write_font(nw: NinjaWriter, output_file: Path, inputs: WriteFontInputs):
    nw.build(
        output_file, "write_font", implicit=list(inputs), variables=inputs._asdict()
    )
    nw.newline()

def _write_config_for_mergeable(
    nw: NinjaWriter, config_file: Path, input_font: Path, color_format: str
):
    nw.build(
        config_file,
        "write_config_for_mergeable",
        input_font,
        variables={"color_format": color_format},
    )
    nw.newline()

def _picosvgs(nw: NinjaWriter, svg_files: List[Path]) -> List[Path]:
    picosvgs = [rel_build(picosvg_dest(s)) for s in svg_files]
    for svg_file, picosvg in zip(svg_files, picosvgs):
        nw.build(picosvg, "picosvg", svg_file)
    nw.newline()
    return picosvgs

def _part_file(
    nw: NinjaWriter, font_config: config.FontConfig, picosvg_files: List[Path]
) -> Path:
    part_files = [part_file_dest(p) for p in picosvg_files]
    for picosvg_file, part_file in zip(picosvg_files, part_files):
        nw.build(
            part_file,
            "write_part_file",
            picosvg_file,
            variables={
                "reuse_tolerance": font_config.reuse_tolerance,
                "wh": font_config.ascender - font_config.descender,
            },
        )

    nw.build(
        master_part_file_dest(),
        "write_combined_part_files",
        sorted(part_files),
    )

    return master_part_file_dest()

def _generate_additional_color_table(
    nw: NinjaWriter,
    input_font: Path,
    glyphmap_inputs: List[Path],
    table_tag: str,
    glue_target: Path,
) -> Path:
    write_font_inputs = WriteFontInputs.for_tag(table_tag)
    identifier = write_font_inputs.color_format
    del table_tag

    # make a glyphmap
    nw.build(
        write_font_inputs.glyphmap_file,
        "write_glyphmap_for_glyph_svgs",
        glyphmap_inputs,
    )
    nw.newline()

    # picosvg because we want good woff2 outcomes
    _write_config_for_mergeable(
        nw, write_font_inputs.config_file, input_font, write_font_inputs.color_format
    )

    # generate a new font with SVG glyphs that use the same names as the original
    font_with_new_table = Path("MergeSource." + identifier + ".ttf")
    _write_font(nw, font_with_new_table, write_font_inputs)

    # stick our shiny new table onto the input font
    output_file = Path(input_font.stem + f".added_{identifier}.ttf")
    nw.build(
        output_file,
        "glue_together",
        font_with_new_table,
        implicit=list({input_font, glue_target}),
        variables={
            "color_table": write_font_inputs.table_tag.strip(),
            "target_font": glue_target,
        },
    )
    nw.newline()

    return output_file

def _generate_svg_from_colr(
    nw: NinjaWriter,
    font_config: config.FontConfig,
    input_font: Path,
    font: ttLib.TTFont,
) -> Tuple[Path, List[Path]]:
    # generate svgs
    svg_files = [
        rel_build(svg_generate_dir() / f"{gid:05d}.svg") for gid in colr_glyphs(font)
    ]
    nw.build(svg_files, "generate_svgs_from_colr", input_font)
    nw.newline()

    # create and merge an SVG table
    picosvgs = _picosvgs(nw, svg_files)
    part_file = _part_file(nw, font_config, picosvgs)

    output_file = _generate_additional_color_table(
        nw, input_font, picosvgs + [input_font], "SVG ", input_font
    )
    return output_file, picosvgs

def _generate_colr_from_svg(
    nw: NinjaWriter,
    font_config: config.FontConfig,
    input_font: Path,
    font: ttLib.TTFont,
) -> Tuple[Path, List[Path]]:
    # extract the svgs
    svg_files = [
        rel_build(svg_extract_dir() / f"{gid:05d}.svg") for gid, _ in svg_glyphs(font)
    ]
    nw.build(svg_files, "extract_svgs_from_otsvg", input_font)
    nw.newline()

    # create and merge a COLR table
    picosvgs = _picosvgs(nw, svg_files)
    part_file = _part_file(nw, font_config, picosvgs)

    output_file = _generate_additional_color_table(
        nw, input_font, picosvgs + [input_font], "COLR", input_font
    )
    return output_file, picosvgs

def _generate_cbdt(
    nw: NinjaWriter,
    input_font: Path,
    font: ttLib.TTFont,
    color_font: Path,
    picosvg_files: List[Path],
):
    # generate bitmaps
    bitmap_files = [rel_build(bitmap_dest(s)) for s in picosvg_files]
    for picosvg, bitmap in zip(picosvg_files, bitmap_files):
        nw.build(bitmap, "write_bitmap", picosvg)
    nw.newline()

    # create and merge a COLR table
    output_file = _generate_additional_color_table(
        nw, input_font, picosvg_files + bitmap_files + [input_font], "CBDT", color_font
    )
    return output_file

def _keep_glyph_names(nw: NinjaWriter, input_file: Path) -> ttLib.TTFont:
    # The whole concept is we keep glyph name stable until the end so
    # make sure we start with stable names. Doesn't matter what they are,
    # just that they don't change.
    output_file = Path(input_file.stem + ".keep_glyph_names.ttf")
    nw.build(
        output_file,
        "keep_glyph_names",
        input_file,
    )
    nw.newline()
    return output_file

def _strip_glyph_names(nw: NinjaWriter, input_file: Path, output_file: Path):
    nw.build(
        output_file,
        "strip_glyph_names",
        input_file,
    )
    nw.newline()

def _run(argv):
    if len(argv) != 2:
        raise ValueError("Must have one argument, a font file")

    if not FLAGS.destroy_non_color_glyphs:
        raise NotImplementedError("Retention of non-color glyphs not implemented yet")

    input_file = Path(argv[1]).resolve()  # we need a non-relative path
    assert input_file.is_file()
    font = ttLib.TTFont(input_file)
    font_config = config.load()
    final_output = Path(font_config.output_file)
    assert (
        input_file.resolve() != (build_dir() / final_output).resolve()
    ), "In == Out is bad"

    build_file = build_dir() / "build.ninja"
    build_dir().mkdir(parents=True, exist_ok=True)

    color_table = _vector_color_table(font)

    if gen_ninja():
        logging.info(f"Generating {build_file.relative_to(build_dir())}")
        with open(build_file, "w") as f:
            nw = NinjaWriter(f)
            _write_preamble(nw)

            wip_file = _keep_glyph_names(nw, input_file)

            # generate the missing vector table
            if color_table == "COLR":
                wip_file, picosvg_files = _generate_svg_from_colr(
                    nw, font_config, wip_file, font
                )
            else:
                wip_file, picosvg_files = _generate_colr_from_svg(
                    nw, font_config, wip_file, font
                )

            if FLAGS.bitmaps:
                wip_file = _generate_cbdt(nw, input_file, font, wip_file, picosvg_files)

            if config.load().keep_glyph_names:
                nw.build(final_output, "copy", wip_file)
            else:
                _strip_glyph_names(nw, wip_file, final_output)

    maybe_run_ninja(build_file)

def main():
    # We don't seem to be __main__ when run as cli tool installed by setuptools
    app.run(_run)

if __name__ == "__main__":
    app.run(_run)
<commit_msg>fix minor comment [skip ci]<commit_after># Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Generates color tables to provide support for a wide range of browsers.

Requires input font have vector color capability, that is either an SVG table
or COLR/CPAL.

https://www.youtube.com/watch?v=HLddvNiXym4

Use cbdt for bitmaps because sbix is less x-platform than you'd guess
(https://github.com/harfbuzz/harfbuzz/issues/2679)

Sample usage:

maximum_color MySvgFont.ttf"""
from absl import app
from absl import flags
from absl import logging
from fontTools import ttLib
from fontTools.ttLib.ttFont import newTable
from nanoemoji import config
from nanoemoji.colr_to_svg import colr_glyphs
from nanoemoji.extract_svgs import svg_glyphs
from nanoemoji.ninja import (
    build_dir,
    gen_ninja,
    maybe_run_ninja,
    module_rule,
    rel_build,
    NinjaWriter,
)
from nanoemoji.util import only
from pathlib import Path
from typing import List, NamedTuple, Tuple

FLAGS = flags.FLAGS

flags.DEFINE_bool(
    "destroy_non_color_glyphs",
    True,
    "If true feel free to obliterate any existing glyf/cff content, e.g. fallback glyphs",
)
flags.DEFINE_bool(
    "bitmaps",
    False,
    "If true, generate a bitmap table (specificaly CBDT)",
)

# attribute names need to match inputs to write_font rule
class WriteFontInputs(NamedTuple):
    glyphmap_file: Path
    config_file: Path
    part_file: Path

    @property
    def table_tag(self) -> str:
        return f"{Path(self.glyphmap_file).stem:4}"

    @property
    def color_format(self) -> str:
        identifier = self.table_tag.strip().lower()

        if identifier == "svg":
            # for good woff2 performance, at cost of inflated size
            return "picosvg"
        elif identifier == "colr":
            # optimize for woff2 performance
            return "glyf_colr_1"
        elif identifier == "cbdt":
            return "cbdt"
        else:
            raise ValueError(f"What is {identifier}?!")

    @classmethod
    def for_tag(cls, table_tag: str) -> "WriteFontInputs":
        basename = table_tag.strip()
        return cls(
            Path(basename + ".glyphmap"),
            Path(basename + ".toml"),
            master_part_file_dest(),
        )

def _vector_color_table(font: ttLib.TTFont) -> str:
    has_svg = "SVG " in font
    has_colr = "COLR" in font
    if has_svg == has_colr:
        raise ValueError("Must have exactly one of COLR, SVG")

    if has_svg:
        return "SVG "
    if has_colr:
        return "COLR"

    raise AssertionError("Impossible")

def svg_extract_dir() -> Path:
    return build_dir() / "svg_dump"

def svg_generate_dir() -> Path:
    return build_dir() / "svg_generate"

def picosvg_dir() -> Path:
    return build_dir() / "picosvg"

def picosvg_dest(input_svg: Path) -> Path:
    return picosvg_dir() / input_svg.name

def part_file_dest(picosvg_file: Path) -> Path:
    return picosvg_file.with_suffix(".parts.json")

def master_part_file_dest() -> Path:
    return Path("parts-merged.json")

def bitmap_dir() -> Path:
    return build_dir() / "bitmap"

def bitmap_dest(input_svg: Path) -> Path:
    return bitmap_dir() / input_svg.with_suffix(".png").name

def _write_preamble(nw: NinjaWriter):
    module_rule(
        nw,
        "extract_svgs_from_otsvg",
        f"--output_dir {rel_build(svg_extract_dir())} $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "generate_svgs_from_colr",
        f"--output_dir {rel_build(svg_generate_dir())} $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "write_glyphmap_for_glyph_svgs",
        f"--output_file $out @$out.rsp",
        rspfile="$out.rsp",
        rspfile_content="$in",
    )
    nw.newline()

    module_rule(
        nw,
        "write_config_for_mergeable",
        "--color_format $color_format $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "write_font",
        f"--config_file $config_file --glyphmap_file $glyphmap_file  --part_file $part_file --output_file $out",
    )
    nw.newline()

    nw.rule(
        f"picosvg",
        f"picosvg --output_file $out $in",
    )
    nw.newline()

    module_rule(
        nw,
        "write_part_file",
        f"--reuse_tolerance $reuse_tolerance --wh $wh --output_file $out $in",
    )
    nw.newline()

    module_rule(
        nw,
        "write_combined_part_files",
        f"--output_file $out  @$out.rsp",
        rspfile="$out.rsp",
        rspfile_content="$in",
    )
    nw.newline()

    # set height only, let width scale proportionally
    res = config.load().bitmap_resolution
    nw.rule(
        "write_bitmap",
        f"resvg -h {res} $in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "glue_together",
        f"--color_table $color_table --target_font $target_font --donor_font $in --output_file $out",
    )
    nw.newline()

    module_rule(
        nw,
        "keep_glyph_names",
        f"$in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "strip_glyph_names",
        f"$in $out",
    )
    nw.newline()

    module_rule(
        nw,
        "copy",
        f"$in $out",
    )
    nw.newline()

def _write_font(nw: NinjaWriter, output_file: Path, inputs: WriteFontInputs):
    nw.build(
        output_file, "write_font", implicit=list(inputs), variables=inputs._asdict()
    )
    nw.newline()

def _write_config_for_mergeable(
    nw: NinjaWriter, config_file: Path, input_font: Path, color_format: str
):
    nw.build(
        config_file,
        "write_config_for_mergeable",
        input_font,
        variables={"color_format": color_format},
    )
    nw.newline()

def _picosvgs(nw: NinjaWriter, svg_files: List[Path]) -> List[Path]:
    picosvgs = [rel_build(picosvg_dest(s)) for s in svg_files]
    for svg_file, picosvg in zip(svg_files, picosvgs):
        nw.build(picosvg, "picosvg", svg_file)
    nw.newline()
    return picosvgs

def _part_file(
    nw: NinjaWriter, font_config: config.FontConfig, picosvg_files: List[Path]
) -> Path:
    part_files = [part_file_dest(p) for p in picosvg_files]
    for picosvg_file, part_file in zip(picosvg_files, part_files):
        nw.build(
            part_file,
            "write_part_file",
            picosvg_file,
            variables={
                "reuse_tolerance": font_config.reuse_tolerance,
                "wh": font_config.ascender - font_config.descender,
            },
        )

    nw.build(
        master_part_file_dest(),
        "write_combined_part_files",
        sorted(part_files),
    )

    return master_part_file_dest()

def _generate_additional_color_table(
    nw: NinjaWriter,
    input_font: Path,
    glyphmap_inputs: List[Path],
    table_tag: str,
    glue_target: Path,
) -> Path:
    write_font_inputs = WriteFontInputs.for_tag(table_tag)
    identifier = write_font_inputs.color_format
    del table_tag

    # make a glyphmap
    nw.build(
        write_font_inputs.glyphmap_file,
        "write_glyphmap_for_glyph_svgs",
        glyphmap_inputs,
    )
    nw.newline()

    # picosvg because we want good woff2 outcomes
    _write_config_for_mergeable(
        nw, write_font_inputs.config_file, input_font, write_font_inputs.color_format
    )

    # generate a new font with SVG glyphs that use the same names as the original
    font_with_new_table = Path("MergeSource." + identifier + ".ttf")
    _write_font(nw, font_with_new_table, write_font_inputs)

    # stick our shiny new table onto the input font
    output_file = Path(input_font.stem + f".added_{identifier}.ttf")
    nw.build(
        output_file,
        "glue_together",
        font_with_new_table,
        implicit=list({input_font, glue_target}),
        variables={
            "color_table": write_font_inputs.table_tag.strip(),
            "target_font": glue_target,
        },
    )
    nw.newline()

    return output_file

def _generate_svg_from_colr(
    nw: NinjaWriter,
    font_config: config.FontConfig,
    input_font: Path,
    font: ttLib.TTFont,
) -> Tuple[Path, List[Path]]:
    # generate svgs
    svg_files = [
        rel_build(svg_generate_dir() / f"{gid:05d}.svg") for gid in colr_glyphs(font)
    ]
    nw.build(svg_files, "generate_svgs_from_colr", input_font)
    nw.newline()

    # create and merge an SVG table
    picosvgs = _picosvgs(nw, svg_files)
    part_file = _part_file(nw, font_config, picosvgs)

    output_file = _generate_additional_color_table(
        nw, input_font, picosvgs + [input_font], "SVG ", input_font
    )
    return output_file, picosvgs

def _generate_colr_from_svg(
    nw: NinjaWriter,
    font_config: config.FontConfig,
    input_font: Path,
    font: ttLib.TTFont,
) -> Tuple[Path, List[Path]]:
    # extract the svgs
    svg_files = [
        rel_build(svg_extract_dir() / f"{gid:05d}.svg") for gid, _ in svg_glyphs(font)
    ]
    nw.build(svg_files, "extract_svgs_from_otsvg", input_font)
    nw.newline()

    # create and merge a COLR table
    picosvgs = _picosvgs(nw, svg_files)
    part_file = _part_file(nw, font_config, picosvgs)

    output_file = _generate_additional_color_table(
        nw, input_font, picosvgs + [input_font], "COLR", input_font
    )
    return output_file, picosvgs

def _generate_cbdt(
    nw: NinjaWriter,
    input_font: Path,
    font: ttLib.TTFont,
    color_font: Path,
    picosvg_files: List[Path],
):
    # generate bitmaps
    bitmap_files = [rel_build(bitmap_dest(s)) for s in picosvg_files]
    for picosvg, bitmap in zip(picosvg_files, bitmap_files):
        nw.build(bitmap, "write_bitmap", picosvg)
    nw.newline()

    # create and merge a CBDT table
    output_file = _generate_additional_color_table(
        nw, input_font, picosvg_files + bitmap_files + [input_font], "CBDT", color_font
    )
    return output_file

def _keep_glyph_names(nw: NinjaWriter, input_file: Path) -> ttLib.TTFont:
    # The whole concept is we keep glyph name stable until the end so
    # make sure we start with stable names. Doesn't matter what they are,
    # just that they don't change.
    output_file = Path(input_file.stem + ".keep_glyph_names.ttf")
    nw.build(
        output_file,
        "keep_glyph_names",
        input_file,
    )
    nw.newline()
    return output_file

def _strip_glyph_names(nw: NinjaWriter, input_file: Path, output_file: Path):
    nw.build(
        output_file,
        "strip_glyph_names",
        input_file,
    )
    nw.newline()

def _run(argv):
    if len(argv) != 2:
        raise ValueError("Must have one argument, a font file")

    if not FLAGS.destroy_non_color_glyphs:
        raise NotImplementedError("Retention of non-color glyphs not implemented yet")

    input_file = Path(argv[1]).resolve()  # we need a non-relative path
    assert input_file.is_file()
    font = ttLib.TTFont(input_file)
    font_config = config.load()
    final_output = Path(font_config.output_file)
    assert (
        input_file.resolve() != (build_dir() / final_output).resolve()
    ), "In == Out is bad"

    build_file = build_dir() / "build.ninja"
    build_dir().mkdir(parents=True, exist_ok=True)

    color_table = _vector_color_table(font)

    if gen_ninja():
        logging.info(f"Generating {build_file.relative_to(build_dir())}")
        with open(build_file, "w") as f:
            nw = NinjaWriter(f)
            _write_preamble(nw)

            wip_file = _keep_glyph_names(nw, input_file)

            # generate the missing vector table
            if color_table == "COLR":
                wip_file, picosvg_files = _generate_svg_from_colr(
                    nw, font_config, wip_file, font
                )
            else:
                wip_file, picosvg_files = _generate_colr_from_svg(
                    nw, font_config, wip_file, font
                )

            if FLAGS.bitmaps:
                wip_file = _generate_cbdt(nw, input_file, font, wip_file, picosvg_files)

            if config.load().keep_glyph_names:
                nw.build(final_output, "copy", wip_file)
            else:
                _strip_glyph_names(nw, wip_file, final_output)

    maybe_run_ninja(build_file)

def main():
    # We don't seem to be __main__ when run as cli tool installed by setuptools
    app.run(_run)

if __name__ == "__main__":
    app.run(_run)
<|endoftext|>


<commit_before>import dataclasses

@dataclasses.dataclass(frozen=True)
class Point:
    x: int = 0
    y: int = 0

@dataclasses.dataclass(frozen=True)
class Rect:
    x: float = 0
    y: float = 0
    w: float = 0
    h: float = 0
<commit_msg>typed tuples are a thing!<commit_after>from typing import NamedTuple

class Point(NamedTuple):
    x: int = 0
    y: int = 0

class Rect(NamedTuple):
    x: float = 0
    y: float = 0
    w: float = 0
    h: float = 0
<|endoftext|>