<commit_before>"""
Support the OwnTracks platform.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks/
"""
import json
import logging
import threading
import base64
from collections import defaultdict

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME
from homeassistant.util import convert, slugify
from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA

REQUIREMENTS = ['libnacl==1.5.0']

_LOGGER = logging.getLogger(__name__)

BEACON_DEV_ID = 'beacon'

CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'

DEPENDENCIES = ['mqtt']

EVENT_TOPIC = 'owntracks/+/+/event'

LOCATION_TOPIC = 'owntracks/+/+'
LOCK = threading.Lock()

MOBILE_BEACONS_ACTIVE = defaultdict(list)

REGIONS_ENTERED = defaultdict(list)

VALIDATE_LOCATION = 'location'
VALIDATE_TRANSITION = 'transition'
VALIDATE_WAYPOINTS = 'waypoints'

WAYPOINT_LAT_KEY = 'lat'
WAYPOINT_LON_KEY = 'lon'
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
    vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
    vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
        cv.ensure_list, [cv.string]),
    vol.Optional(CONF_SECRET): vol.Any(
        vol.Schema({vol.Optional(cv.string): cv.string}),
        cv.string)
})


def get_cipher():
    """Return decryption function and length of key."""
    from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
    from libnacl.secret import SecretBox

    def decrypt(ciphertext, key):
        """Decrypt ciphertext using key."""
        return SecretBox(key).decrypt(ciphertext)
    return (KEYLEN, decrypt)


def setup_scanner(hass, config, see):
    """Set up an OwnTracks tracker."""
    max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
    waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
    waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
    secret = config.get(CONF_SECRET)

    def decrypt_payload(topic, ciphertext):
        """Decrypt encrypted payload."""
        try:
            keylen, decrypt = get_cipher()
        except OSError:
            _LOGGER.warning('Ignoring encrypted payload '
                            'because libsodium not installed.')
            return None

        if isinstance(secret, dict):
            key = secret.get(topic)
        else:
            key = secret

        if key is None:
            _LOGGER.warning('Ignoring encrypted payload '
                            'because no decryption key known '
                            'for topic %s.', topic)
            return None

        key = key.encode("utf-8")
        key = key[:keylen]
        key = key.ljust(keylen, b'\0')

        try:
            ciphertext = base64.b64decode(ciphertext)
            message = decrypt(ciphertext, key)
            message = message.decode("utf-8")
            _LOGGER.debug("Decrypted payload: %s", message)
            return message
        except ValueError:
            _LOGGER.warning('Ignoring encrypted payload '
                            'because unable to decrypt using key '
                            'for topic %s.', topic)
            return None

    # pylint: disable=too-many-return-statements
    def validate_payload(topic, payload, data_type):
        """Validate the OwnTracks payload."""
        try:
            data = json.loads(payload)
        except ValueError:
            # If invalid JSON
            _LOGGER.error('Unable to parse payload as JSON: %s', payload)
            return None

        if isinstance(data, dict) and \
           data.get('_type') == 'encrypted' and \
           'data' in data:
            plaintext_payload = decrypt_payload(topic, data['data'])
            if plaintext_payload is None:
                return None
            else:
                return validate_payload(topic, plaintext_payload, data_type)

        if not isinstance(data, dict) or data.get('_type') != data_type:
            _LOGGER.debug('Skipping %s update for following data '
                          'because of missing or malformatted data: %s',
                          data_type, data)
            return None
        if data_type == VALIDATE_TRANSITION or data_type == VALIDATE_WAYPOINTS:
            return data
        if max_gps_accuracy is not None and \
                convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
            _LOGGER.info('Ignoring %s update because expected GPS '
                         'accuracy %s is not met: %s',
                         data_type, max_gps_accuracy, payload)
            return None
        if convert(data.get('acc'), float, 1.0) == 0.0:
            _LOGGER.warning('Ignoring %s update because GPS accuracy'
                            'is zero: %s',
                            data_type, payload)
            return None

        return data

    def owntracks_location_update(topic, payload, qos):
        """MQTT message received."""
        # Docs on available data:
        # http://owntracks.org/booklet/tech/json/#_typelocation
        data = validate_payload(topic, payload, VALIDATE_LOCATION)
        if not data:
            return

        dev_id, kwargs = _parse_see_args(topic, data)

        # Block updates if we're in a region
        with LOCK:
            if REGIONS_ENTERED[dev_id]:
                _LOGGER.debug(<commit_msg>Fix missing space in error message between "accuracy" and "is" (#4542)<commit_after>"""
Support the OwnTracks platform.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks/
"""
import json
import logging
import threading
import base64
from collections import defaultdict

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME
from homeassistant.util import convert, slugify
from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA

REQUIREMENTS = ['libnacl==1.5.0']

_LOGGER = logging.getLogger(__name__)

BEACON_DEV_ID = 'beacon'

CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'

DEPENDENCIES = ['mqtt']

EVENT_TOPIC = 'owntracks/+/+/event'

LOCATION_TOPIC = 'owntracks/+/+'
LOCK = threading.Lock()

MOBILE_BEACONS_ACTIVE = defaultdict(list)

REGIONS_ENTERED = defaultdict(list)

VALIDATE_LOCATION = 'location'
VALIDATE_TRANSITION = 'transition'
VALIDATE_WAYPOINTS = 'waypoints'

WAYPOINT_LAT_KEY = 'lat'
WAYPOINT_LON_KEY = 'lon'
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
    vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
    vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
        cv.ensure_list, [cv.string]),
    vol.Optional(CONF_SECRET): vol.Any(
        vol.Schema({vol.Optional(cv.string): cv.string}),
        cv.string)
})


def get_cipher():
    """Return decryption function and length of key."""
    from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
    from libnacl.secret import SecretBox

    def decrypt(ciphertext, key):
        """Decrypt ciphertext using key."""
        return SecretBox(key).decrypt(ciphertext)
    return (KEYLEN, decrypt)


def setup_scanner(hass, config, see):
    """Set up an OwnTracks tracker."""
    max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
    waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
    waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
    secret = config.get(CONF_SECRET)

    def decrypt_payload(topic, ciphertext):
        """Decrypt encrypted payload."""
        try:
            keylen, decrypt = get_cipher()
        except OSError:
            _LOGGER.warning('Ignoring encrypted payload '
                            'because libsodium not installed.')
            return None

        if isinstance(secret, dict):
            key = secret.get(topic)
        else:
            key = secret

        if key is None:
            _LOGGER.warning('Ignoring encrypted payload '
                            'because no decryption key known '
                            'for topic %s.', topic)
            return None

        key = key.encode("utf-8")
        key = key[:keylen]
        key = key.ljust(keylen, b'\0')

        try:
            ciphertext = base64.b64decode(ciphertext)
            message = decrypt(ciphertext, key)
            message = message.decode("utf-8")
            _LOGGER.debug("Decrypted payload: %s", message)
            return message
        except ValueError:
            _LOGGER.warning('Ignoring encrypted payload '
                            'because unable to decrypt using key '
                            'for topic %s.', topic)
            return None

    # pylint: disable=too-many-return-statements
    def validate_payload(topic, payload, data_type):
        """Validate the OwnTracks payload."""
        try:
            data = json.loads(payload)
        except ValueError:
            # If invalid JSON
            _LOGGER.error('Unable to parse payload as JSON: %s', payload)
            return None

        if isinstance(data, dict) and \
           data.get('_type') == 'encrypted' and \
           'data' in data:
            plaintext_payload = decrypt_payload(topic, data['data'])
            if plaintext_payload is None:
                return None
            else:
                return validate_payload(topic, plaintext_payload, data_type)

        if not isinstance(data, dict) or data.get('_type') != data_type:
            _LOGGER.debug('Skipping %s update for following data '
                          'because of missing or malformatted data: %s',
                          data_type, data)
            return None
        if data_type == VALIDATE_TRANSITION or data_type == VALIDATE_WAYPOINTS:
            return data
        if max_gps_accuracy is not None and \
                convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
            _LOGGER.info('Ignoring %s update because expected GPS '
                         'accuracy %s is not met: %s',
                         data_type, max_gps_accuracy, payload)
            return None
        if convert(data.get('acc'), float, 1.0) == 0.0:
            _LOGGER.warning('Ignoring %s update because GPS accuracy '
                            'is zero: %s',
                            data_type, payload)
            return None

        return data

    def owntracks_location_update(topic, payload, qos):
        """MQTT message received."""
        # Docs on available data:
        # http://owntracks.org/booklet/tech/json/#_typelocation
        data = validate_payload(topic, payload, VALIDATE_LOCATION)
        if not data:
            return

        dev_id, kwargs = _parse_see_args(topic, data)

        # Block updates if we're in a region
        with LOCK:
            if REGIONS_ENTERED[dev_id]:
                _LOGGER.debug(
{'fraction_non_alphanumeric': 0.09128303868370383, 'fraction_numerical': 0.003500787677227376, 'mean_word_length': 2.378769958604376, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2008403', 'n_tokens_mistral': 3418, 'n_tokens_neox': 3368, 'n_words': 839}