Clean up context ID generation.

Using a sha256 hash is major overkill for the purposes of finding
a short hash of a string. This change switches _short_hash to use
Python's built-in hash() function (used for hashing in dicts and sets).
which works just fine for this, and is much faster.

In addition, since context IDs are length-limited, use base-85 encoding
to pack as many bits of the hash and index into the context as possible.
This commit is contained in:
Justin Paupore 2020-10-27 21:41:33 -07:00
commit c785dcd202

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
import base64
import bisect
from collections import defaultdict
from copy import deepcopy
@ -9,7 +10,6 @@ from dataclasses import dataclass
import datetime
from datetime import timedelta
import functools
import hashlib
import logging
import math
from typing import Any, Dict, List, Optional, Tuple, Union
@ -163,10 +163,17 @@ BRIGHTNESS_ATTRS = {
# Keep a short domain version for the context instances (which can only be 36 chars)
_DOMAIN_SHORT = "adapt_lgt"
def _int_to_bytes(i: int, signed: bool = False) -> bytes:
bits = i.bit_length()
if signed:
# Make room for the sign bit.
bits += 1
return i.to_bytes((bits + 7) // 8, 'little', signed=signed)
def _short_hash(string: str, length: int = 4) -> str:
"""Create a hash of 'string' with length 'length'."""
return hashlib.sha1(string.encode("UTF-8")).hexdigest()[:length]
str_hash_bytes = _int_to_bytes(hash(string), signed=True)
return base64.b85encode(str_hash_bytes)[:length]
def create_context(name: str, which: str, index: int) -> Context:
@ -174,7 +181,11 @@ def create_context(name: str, which: str, index: int) -> Context:
# Use a hash for the name because otherwise the context might become
# too long (max len == 36) to fit in the database.
name_hash = _short_hash(name)
return Context(id=f"{_DOMAIN_SHORT}_{name_hash}_{which}_{index}")
# Pack index with base85 to maximize the number of contexts we can create
# before we exceed the 36-character limit and are forced to wrap.
index_packed = base64.b85encode(_int_to_bytes(index, signed=False))
context_id = f"{_DOMAIN_SHORT}:{name_hash}:{which}:{index_packed}"[:36]
return Context(id=context_id)
def is_our_context(context: Optional[Context]) -> bool:
@ -635,13 +646,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
def create_context(self, which: str = "default") -> Context:
"""Create a context that identifies this Adaptive Lighting instance."""
# Right now the highest number of each context_id it can create is
# 'adapt_lgt_XXXX_turn_on_9999999999999'
# 'adapt_lgt_XXXX_interval_999999999999'
# 'adapt_lgt_XXXX_adapt_lights_99999999'
# 'adapt_lgt_XXXX_sleep_999999999999999'
# 'adapt_lgt_XXXX_light_event_999999999'
# 'adapt_lgt_XXXX_service_9999999999999'
# So 100 million calls before we run into the 36 chars limit.
# 'adapt_lgt:XXXX:turn_on:*************'
# 'adapt_lgt:XXXX:interval:************'
# 'adapt_lgt:XXXX:adapt_lights:********'
# 'adapt_lgt:XXXX:sleep:***************'
# 'adapt_lgt:XXXX:light_event:*********'
# 'adapt_lgt:XXXX:service:*************'
# The smallest space we have is for adapt_lights, which has
# 8 characters. In base85 encoding, that's enough space to hold values
# up to 2**48 - 1, which should give us plenty of calls before we wrap.
context = create_context(self._name, which, self._context_cnt)
self._context_cnt += 1
return context