def attach_keg(self, tap, keg): """Activates a keg at the given tap (with existing choices). The tap must be inactive (tap.current_keg == None), otherwise a ValueError will be thrown. Args: tap: The KegTap or meter name to tap against. keg_id: The Keg ID to map to tap. Returns: The keg instance. """ tap = self._get_tap(tap) if tap.is_active(): raise ValueError('Tap is already active, must end keg first.') keg.start_time = timezone.now() keg.online = True keg.save() old_keg = tap.current_keg if old_keg: self.end_keg(tap) tap.current_keg = keg tap.save() events = models.SystemEvent.build_events_for_keg(keg) tasks.schedule_tasks(events) signals.keg_attached.send_robust(sender=self.__class__, keg=keg, tap=tap) return keg
def end_keg(self, tap): """Takes the current Keg offline at the given tap. Args: tap: The KegTap object to tap against, or a string matching the tap's meter name. Returns: The old keg. Raises: ValueError: if the tap is missing or already offline. """ tap = self._get_tap(tap) if not tap.current_keg: raise ValueError('Tap is already offline.') keg = tap.current_keg keg.online = False keg.end_time = timezone.now() keg.finished = True keg.save() tap.current_keg = None tap.save() events = models.SystemEvent.build_events_for_keg(keg) tasks.schedule_tasks(events) signals.keg_ended.send_robust(sender=self.__class__, keg=keg) return keg
def end_keg(self, tap): """Takes the current Keg offline at the given tap. Args: tap: The KegTap object to tap against, or a string matching KegTap.meter_name. Returns: The old keg. Raises: ValueError: if the tap is missing or already offline. """ if not isinstance(tap, models.KegTap): tap = models.KegTap.objects.get(meter_name=tap) if not tap.current_keg: raise ValueError('Tap is already offline.') keg = tap.current_keg keg.online = False keg.save() tap.current_keg = None tap.save() events = models.SystemEvent.build_events_for_keg(keg) tasks.schedule_tasks(events) return keg
def end_keg(self, keg, when=None): """Takes the given keg offline.""" if not when: when = timezone.now() if keg.status == keg.STATUS_ON_TAP: keg = self.disconnect_keg(keg) keg.status = keg.STATUS_FINISHED keg.end_time = when keg.save() events = models.SystemEvent.build_events_for_keg(keg) tasks.schedule_tasks(events) signals.keg_ended.send_robust(sender=self.__class__, keg=keg) return keg
def attach_keg(self, tap, keg): """Activates a new keg at the given tap (with existing choices). The tap must be inactive (tap.current_keg == None), otherwise a ValueError will be thrown. Since the keg already exists in the database, it is assumed that "add_keg" took care of all the error conditions and checking. Args: tap: The KegTap or meter name to tap against. keg_id: The Keg ID to map to tap. Returns: The new keg instance. """ with transaction.atomic(): tap = self._get_tap(tap) if tap.is_active(): raise ValueError('Tap is already active, must end keg first.') keg.start_time = timezone.now() keg.online = True keg.save() old_keg = tap.current_keg if old_keg: self.end_keg(tap) tap.current_keg = keg tap.save() events = models.SystemEvent.build_events_for_keg(keg) with transaction.atomic(): tasks.schedule_tasks(events) return keg
class KegbotBackend: """Provides high-level operations against the Kegbot system.""" def __init__(self): self._logger = logging.getLogger('backend') self.cache = KegbotCache() @transaction.commit_on_success def CreateNewUser(self, username): """Creates and returns a User for the given username.""" return models.User.objects.create(username=username) @transaction.commit_on_success def CreateTap(self, name, meter_name, relay_name=None, ml_per_tick=None): """Creates and returns a new KegTap. Args: name: The human-meaningful name for the tap, for instance, "Main Tap". meter_name: The unique sensor name for the tap, for instance, "kegboard.flow0". relay_name: If the tap is connected to a relay, this specifies its name, for instance, "kegboard.relay0". ml_per_tick: The number of milliliters per flow meter tick on this tap's meter. Returns: The new KegTap instance. """ tap = models.KegTap.objects.create(name=name, meter_name=meter_name, relay_name=relay_name, ml_per_tick=ml_per_tick) tap.save() return tap @transaction.commit_on_success def CreateAuthToken(self, auth_device, token_value, username=None): """Creates a new AuthenticationToken. The combination of (auth_device, token_value) must be unique within the system. Args: auth_device: The name of the authentication device, for instance, "core.rfid". token_value: The opaque string value of the token, which is typically unique within the `auth_device` namespace. username: The User with which to associate this Token. Returns: The newly-created AuthenticationToken. """ token = models.AuthenticationToken.objects.create( auth_device=auth_device, token_value=token_value) if username: user = get_user(username) token.user = user token.save() return token @transaction.commit_on_success def RecordDrink(self, tap_name, ticks, volume_ml=None, username=None, pour_time=None, duration=0, shout='', tick_time_series='', do_postprocess=True): """Records a new drink against a given tap. The tap must have a Keg assigned to it (KegTap.current_keg), and the keg must be active. Args: tap_name: The tap's meter_name. ticks: The number of ticks observed by the flow sensor for this drink. volume_ml: The volume, in milliliters, of the pour. If specifed, this value is saved as the Drink's actual value. If not specified, a volume is computed based on `ticks` and the KegTap's `ml_per_tick` setting. username: The username with which to associate this Drink, or None for an anonymous Drink. pour_time: The datetime of the drink. If not supplied, the current date and time will be used. duration: Number of seconds it took to pour this Drink. This is optional information not critical to the record. shout: A short comment left by the user about this Drink. Optional. tick_time_series: Vector of flow update data, used for diagnostic purposes. do_postprocess: Set to False during bulk inserts. Returns: The newly-created Drink instance. """ tap = self._GetTapFromName(tap_name) if not tap: raise BackendError("Tap unknown") if not tap.is_active or not tap.current_keg: raise BackendError("No active keg at this tap") if volume_ml is None: volume_ml = float(ticks) * tap.ml_per_tick user = None if username: user = get_user(username) else: user = models.SiteSettings.get().default_user if not pour_time: pour_time = timezone.now() keg = tap.current_keg if tick_time_series: try: # Validate the time series by parsing it; canonicalize it by generating # it again. If malformed, just junk it; it's non-essential information. tick_time_series = time_series.to_string( time_series.from_string(tick_time_series)) except ValueError, e: self._logger.warning( 'Time series invalid, ignoring. Error was: %s' % e) tick_time_series = '' d = models.Drink(ticks=ticks, keg=keg, user=user, volume_ml=volume_ml, time=pour_time, duration=duration, shout=shout, tick_time_series=tick_time_series) models.DrinkingSession.AssignSessionForDrink(d) d.save() keg.served_volume_ml += volume_ml keg.save(update_fields=['served_volume_ml']) self.cache.update_generation() if do_postprocess: stats.generate(d) events = models.SystemEvent.ProcessDrink(d) tasks.schedule_tasks(events) return d
def record_drink(self, tap, ticks, volume_ml=None, username=None, pour_time=None, duration=0, shout='', tick_time_series='', photo=None, spilled=False): """Records a new drink against a given tap. The tap must have a Keg assigned to it (KegTap.current_keg), and the keg must be active. Args: tap: A KegTap or matching meter name. ticks: The number of ticks observed by the flow sensor for this drink. volume_ml: The volume, in milliliters, of the pour. If specifed, this value is saved as the Drink's actual value. If not specified, a volume is computed based on `ticks` and the meter's `ticks_per_ml` setting. username: The username with which to associate this Drink, or None for an anonymous Drink. pour_time: The datetime of the drink. If not supplied, the current date and time will be used. duration: Number of seconds it took to pour this Drink. This is optional information not critical to the record. shout: A short comment left by the user about this Drink. Optional. tick_time_series: Vector of flow update data, used for diagnostic purposes. spilled: If drink is recorded as spill, the volume is added to spilled_ml and the "drink" is not saved as an event. Returns: The newly-created Drink instance. """ tap = self._get_tap(tap) if not tap.is_active or not tap.current_keg: raise exceptions.BackendError("No active keg at this tap") keg = tap.current_keg if spilled: keg.spilled_ml += volume_ml keg.save(update_fields=['spilled_ml']) return None if volume_ml is None: meter = tap.current_meter() if not meter: raise exceptions.BackendError("Tap has no meter, can't compute volume") volume_ml = float(ticks) / meter.ticks_per_ml user = None if username: user = self.get_user(username) else: user = models.User.objects.get(username='******') if not pour_time: pour_time = timezone.now() if tick_time_series: try: # Validate the time series by parsing it; canonicalize it by generating # it again. If malformed, just junk it; it's non-essential information. tick_time_series = time_series.to_string(time_series.from_string(tick_time_series)) except ValueError as e: self._logger.warning('Time series invalid, ignoring. Error was: %s' % e) tick_time_series = '' d = models.Drink(ticks=ticks, keg=keg, user=user, volume_ml=volume_ml, time=pour_time, duration=duration, shout=shout, tick_time_series=tick_time_series) models.DrinkingSession.AssignSessionForDrink(d) d.save() keg.served_volume_ml += volume_ml keg.save(update_fields=['served_volume_ml']) if photo: pic = models.Picture.objects.create( image=photo, user=d.user, keg=d.keg, session=d.session ) d.picture = pic d.save() events = models.SystemEvent.build_events_for_drink(d) tasks.schedule_tasks(events) self.build_stats(d.id) signals.drink_recorded.send_robust(sender=self.__class__, drink=d) return d
def start_keg(self, tap, beverage=None, keg_type=keg_sizes.HALF_BARREL, full_volume_ml=None, beverage_name=None, beverage_type=None, producer_name=None, style_name=None, when=None): """Activates a new keg at the given tap. The tap must be inactive (tap.current_keg == None), otherwise a ValueError will be thrown. A beverage must be specified, either by providing an existing Beverage instance as `beverage`, or by specifying values for `beverage_type`, `beverage_name`, `producer_name`, and `style_name`. When using the latter form, the system will attempt to match the string type parameters against an already-existing Beverage. Otherwise, a new Beverage will be created. Args: tap: The KegTap object to tap against, or a string matching KegTap.meter_name. beverage: The type of beverage, as a Beverage object. keg_type: The type of physical keg, from keg_sizes. full_volume_ml: The keg's original unserved volume. If unspecified, will be interpreted from keg_type. It is an error to omit this parameter when keg_type is OTHER. beverage_name: The keg's beverage name. Must be given with `producer_name` and `style_name`; `beverage` must be None. beverage_type: The keg beverage type. producer_name: The brewer or producer of this beverage. style_name: The style of this beverage. when: Keg activation date and time. If not specified, current time will be used. Returns: The new keg instance. """ if not isinstance(tap, models.KegTap): tap = models.KegTap.objects.get(meter_name=tap) if tap.is_active(): raise ValueError('Tap is already active, must end keg first.') if beverage: if beverage_type or beverage_name or producer_name or style_name: raise ValueError( 'Cannot give beverage_type, beverage_name, producer_name, or style_name with beverage') else: if not beverage_type: raise ValueError('Must supply beverage_type when beverage is None') if not beverage_name: raise ValueError('Must supply beverage_name when beverage is None') if not producer_name: raise ValueError('Must supply producer_name when beverage is None') if not style_name: raise ValueError('Must supply style_name when beverage is None') producer = models.BeverageProducer.objects.get_or_create(name=producer_name)[0] beverage = models.Beverage.objects.get_or_create(name=beverage_name, beverage_type=beverage_type, producer=producer, style=style_name)[0] if keg_type not in keg_sizes.DESCRIPTIONS: raise ValueError('Unrecognized keg type: %s' % keg_type) if full_volume_ml is None: full_volume_ml = keg_sizes.VOLUMES_ML[keg_type] if not when: when = timezone.now() keg = models.Keg.objects.create(type=beverage, keg_type=keg_type, online=True, full_volume_ml=full_volume_ml, start_time=when) old_keg = tap.current_keg if old_keg: self.end_keg(tap) tap.current_keg = keg tap.save() events = models.SystemEvent.build_events_for_keg(keg) tasks.schedule_tasks(events) return keg
class KegbotBackend(object): """Provides high-level operations against the Kegbot system.""" def __init__(self): self._logger = logging.getLogger('backend') self.cache = KegbotCache() @transaction.atomic def create_new_user(self, username, email, password=None, photo=None): """Creates and returns a User for the given username.""" try: user = get_auth_backend().register(username=username, email=email, password=password, photo=photo) return user except UserExistsException as e: raise UserExistsError(e) except AuthException as e: raise BackendError(e) @transaction.atomic def create_tap(self, name, meter_name, toggle_name=None, ticks_per_ml=None): """Creates and returns a new KegTap. Args: name: The human-meaningful name for the tap, for instance, "Main Tap". meter_name: The unique sensor name for the tap, for instance, "kegboard.flow0". toggle_name: If the tap is connected to a relay, this specifies its name, for instance, "kegboard.relay0". ticks_per_ml: The number of flow meter ticks per milliliter of fluid on this tap's meter. Returns: The new KegTap instance. """ tap = models.KegTap.objects.create(name=name) tap.save() meter = models.FlowMeter.get_or_create_from_meter_name(meter_name) meter.tap = tap if ticks_per_ml: meter.ticks_per_ml = ticks_per_ml meter.save() if toggle_name: toggle = models.FlowToggle.get_or_create_from_toggle_name(toggle_name) return tap @transaction.atomic def create_auth_token(self, auth_device, token_value, username=None): """Creates a new AuthenticationToken. The combination of (auth_device, token_value) must be unique within the system. Args: auth_device: The name of the authentication device, for instance, "core.rfid". token_value: The opaque string value of the token, which is typically unique within the `auth_device` namespace. username: The User with which to associate this Token. Returns: The newly-created AuthenticationToken. """ token = models.AuthenticationToken.objects.create( auth_device=auth_device, token_value=token_value) if username: user = get_user(username) token.user = user token.save() return token def record_drink(self, tap, ticks, volume_ml=None, username=None, pour_time=None, duration=0, shout='', tick_time_series='', photo=None, do_postprocess=True, spilled=False): """Records a new drink against a given tap. The tap must have a Keg assigned to it (KegTap.current_keg), and the keg must be active. Args: tap: A KegTap or matching meter name. ticks: The number of ticks observed by the flow sensor for this drink. volume_ml: The volume, in milliliters, of the pour. If specifed, this value is saved as the Drink's actual value. If not specified, a volume is computed based on `ticks` and the meter's `ticks_per_ml` setting. username: The username with which to associate this Drink, or None for an anonymous Drink. pour_time: The datetime of the drink. If not supplied, the current date and time will be used. duration: Number of seconds it took to pour this Drink. This is optional information not critical to the record. shout: A short comment left by the user about this Drink. Optional. tick_time_series: Vector of flow update data, used for diagnostic purposes. do_postprocess: Set to False during bulk inserts. spilled: If drink is recorded as spill, the volume is added to spilled_ml and the "drink" is not saved as an event. Returns: The newly-created Drink instance. """ with transaction.atomic(): tap = self._get_tap(tap) if not tap.is_active or not tap.current_keg: raise BackendError("No active keg at this tap") keg = tap.current_keg if spilled: keg.spilled_ml += volume_ml keg.save(update_fields=['spilled_ml']) return None if volume_ml is None: meter = tap.current_meter() if not meter: raise BackendError("Tap has no meter, can't compute volume") volume_ml = float(ticks) / meter.ticks_per_ml user = None if username: user = get_user(username) else: user = models.KegbotSite.get().default_user if not user: user = models.User.objects.get(username='******') if not pour_time: pour_time = timezone.now() if tick_time_series: try: # Validate the time series by parsing it; canonicalize it by generating # it again. If malformed, just junk it; it's non-essential information. tick_time_series = time_series.to_string(time_series.from_string(tick_time_series)) except ValueError, e: self._logger.warning('Time series invalid, ignoring. Error was: %s' % e) tick_time_series = '' d = models.Drink(ticks=ticks, keg=keg, user=user, volume_ml=volume_ml, time=pour_time, duration=duration, shout=shout, tick_time_series=tick_time_series) models.DrinkingSession.AssignSessionForDrink(d) d.save() keg.served_volume_ml += volume_ml keg.save(update_fields=['served_volume_ml']) self.cache.update_generation() if photo: with transaction.atomic(): pic = models.Picture.objects.create( image=photo, user=d.user, keg=d.keg, session=d.session ) d.picture = pic d.save() if do_postprocess: self.build_stats(d.id) with transaction.atomic(): events = models.SystemEvent.build_events_for_drink(d) tasks.schedule_tasks(events) return d