Skip to content

Commit

Permalink
allow creating custom semantic tags
Browse files Browse the repository at this point in the history
Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng committed Mar 31, 2023
1 parent 0db2fef commit 429afbd
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- {OpenHAB::Core::Rules::Registry#scenes rules.scenes} and {OpenHAB::Core::Rules::Registry#scripts rules.scripts}
are available as convenient shortcuts to filter for rules that are scenes or scripts.
- Support creating custom semantic tags with {OpenHAB::Core::Items::Semantics.create Semantics.create}

### Bug Fixes

Expand Down
61 changes: 61 additions & 0 deletions features/semantics.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Feature: semantics
Custom semantic tag creation

Background:
Given Clean OpenHAB with latest Ruby Libraries

Scenario: Semantic tag created
Given a rule
"""
Semantics.create(SecretRoom: Semantics::Room)
logger.info Semantics::SecretRoom.java_class.to_s
"""
When I deploy the rule
Then It should log 'org.openhab.core.semantics.model.location.SecretRoom' within 5 seconds

Scenario: Support creating multiple tags
Given a rule
"""
Semantics.create(SecretRoom: Semantics::Room, Color: Semantics::Property)
logger.info Semantics::SecretRoom.java_class.to_s
logger.info Semantics::Color.java_class.to_s
"""
When I deploy the rule
Then It should log 'org.openhab.core.semantics.model.location.SecretRoom' within 5 seconds
And It should log 'org.openhab.core.semantics.model.property.Color' within 5 seconds

Scenario: Support custom label
Given a rule
"""
Semantics.create(Room1: Semantics::Room, label: "My Custom Label")
logger.info org.openhab.core.semantics.SemanticTags.get_label(Semantics::Room1, java.util.Locale.default)
"""
When I deploy the rule
Then It should log 'My Custom Label' within 5 seconds

# There's a bug in openhab that doesn't load the synonyms other than from the resource bundle
# @wip
# Scenario: Support synonyms
# Given a rule
# """
# Semantics.create(Room2: Semantics::Room, synonyms: "Alias1")
# logger.info org.openhab.core.semantics.SemanticTags.get_by_label_or_synonym("Alias1", java.util.Locale.default)
# """
# When I deploy the rule
# Then It should log 'org.openhab.core.semantics.model.location.Room2' within 5 seconds

Scenario: Semantic tag usable by items
Given a rule
"""
Semantics.create(<tag>: Semantics::<parent>)
items.build { <item_type>_item "<item_name>", tags: Semantics::<tag> }
logger.info "<item_name> is a <semantic_type>? #{<item_name>.<semantic_type>?}"
"""
When I deploy the rule
Then It should log '<item_name> is a <semantic_type>? <result>' within 5 seconds
Examples:
| tag | parent | semantic_type | item_type | item_name | result |
| SecretRoom | Room | location | group | Bunker | true |
| LprCamera | Camera | equipment | group | LPR1 | true |
| HeatingElement | Control | point | dimmer | Heater | true |
| Color | Light | point | color | LightColor | true |
7 changes: 7 additions & 0 deletions lib/openhab/core/items/semantics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ module Items
# # All items tagged "SmartLightControl"
# items.tagged("SmartLightControl")
#
# ## Creating Custom Semantic Tags
#
# This library provides an experimental support for creating custom semantic tags.
# It can be used to augment the standard set of tags from openHAB with additional
# tags that will better suit your particular requirements.
#
# For more information, see {create}
#
module Semantics
GenericItem.include(self)
Expand Down
156 changes: 156 additions & 0 deletions lib/openhab/core/items/semantics/custom_semantic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# frozen_string_literal: true

java_import org.objectweb.asm.ClassWriter
java_import org.objectweb.asm.Opcodes

module OpenHAB
module Core
module Items
module Semantics
#
# Utility to create custom Semantic tags
#
# @!visibility private
class CustomSemantic
class << self
#
# Creates a custom semantic tag.
#
# @param [Symbol,String] name The name of the new semantic tag to add
# @param [String, Semantics::Tag] parent The semantic class of the parent tag
# @param [String] label An optional label. If not provided, it will be generated from name by
# splitting up the CamelCase with a space
# @param [String] synonyms A comma separated list of synonyms.
# @param [String] description A longer description.
#
# @return [Semantics::Tag] The created semantic tag class
#
def create(name, parent, label: nil, synonyms: "", description: "")
return Semantics.const_get(name) if Semantics.constants.include?(name)

name = name.to_s
unless /^[A-Z][_a-zA-Z0-9]+$/.match?(name)
raise "Name must start with a capital letter and not contain any spaces"
end

parent = Semantics.const_get(parent) if parent.is_a?(String)
valid_types = [Semantics::Location, Semantics::Equipment, Semantics::Point, Semantics::Property]
type = valid_types.find { |t| parent == t || parent < t }
raise "Parent must be one of #{valid_types} or their descendants" unless type

type = type.java_class.simple_name.downcase
class_name = "org.openhab.core.semantics.model.#{type}.#{name}"

internal_parent_name = parent.java_class.name.tr(".", "/")
internal_class_name = class_name.tr(".", "/")

# Create the class/interface
class_writer = ClassWriter.new(0)
class_writer.visit(Opcodes::V11, Opcodes::ACC_PUBLIC + Opcodes::ACC_ABSTRACT + Opcodes::ACC_INTERFACE,
internal_class_name, nil, "java/lang/Object", [internal_parent_name])

# Add TagInfo Annotation
class_writer.visit_source("Status.java", nil)
parent.java_class.get_annotation(org.openhab.core.semantics.TagInfo.java_class).id.then do |parent_id|
# CamelCase -> Camel Case, Snake_case->Snake Case
label ||= name.scan(/[a-z]+|[A-Z][a-z]+/).map(&:capitalize).join(" ")

# Correct a bug in openhab, Semantics::Property's id is `MeasurementProperty` instead of Property
parent_id = "Property" if parent_id == "MeasurementProperty"

annotation_visitor = class_writer.visit_annotation("Lorg/openhab/core/semantics/TagInfo;", true)
annotation_visitor.visit("id", "#{parent_id}_#{name}")
annotation_visitor.visit("label", label)
annotation_visitor.visit("synonyms", synonyms)
annotation_visitor.visit("description", description)
annotation_visitor.visit_end
end

class_writer.visit_end
byte_code = class_writer.to_byte_array

java_klass = org.openhab.core.semantics.SemanticTags.java_class.class_loader
.define_class(class_name, byte_code, 0, byte_code.length)

register_tag(java_klass)
add_ruby_constant(java_klass)

java_klass.ruby_class
end

private

def register_tag(java_klass)
id = java_klass.get_annotation(org.openhab.core.semantics.TagInfo.java_class).id
type = id.split("_").first

# Add to org.openhab.core.semantics.model.location.Locations.LOCATIONS
type_plural = "#{type.sub(/y$/, "ie")}s" # pluralize, Property -> Properties, Location -> Locations
field_name = type_plural.upcase.to_sym
type_aggregator = java_import("org.openhab.core.semantics.model.#{type.downcase}.#{type_plural}").first
type_aggregator.field_reader field_name
members_list = type_aggregator.send(field_name)
members_list.add(java_klass)

# Add to org.openhab.core.semantics.SemanticTags.TAGS
# by calling the private method SemanticTags.addTagSet
semantic_tags = org.openhab.core.semantics.SemanticTags.java_class
add_tag_set = semantic_tags.declared_method(:addTagSet, java.lang.Class.java_class)
add_tag_set.accessible = true
add_tag_set.invoke(semantic_tags, java_klass)
add_tag_set.accessible = false
end

def add_ruby_constant(java_klass)
OpenHAB::Core::Items::Semantics.const_set(java_klass.simple_name.to_sym, java_klass.ruby_class)
end
end
end

#
# Create custom semantic tags.
#
# @return [Semantics::Tag] The created semantic tag class
#
# @overload self.create(**tags)
# Quickly create one or more semantic tags using the default label, empty synonyms and descriptions.
#
# @param [kwargs] **tags One or more `tag` => `parent` pairs
# @return [Semantics::Tag] The created semantic tag class
#
# @example Create one semantic tag `Balcony` whose parent is `Semantics::Outdoor` (Location)
# Semantics.create(Balcony: Semantics::Outdoor)
#
# @example Create multiple semantic tags
# Semantics.create(Balcony: Semantics::Outdoor,
# SecretRoom: Semantics::Room,
# Motion: Semantics::Property)
#
# @overload self.create(label: nil, synonyms: "", description: "", **tags)
# Create a custom semantic tag with custom details.
#
# @example
# Semantics.create(SecretRoom: Semantics::Room, label: "My Secret Room",
# synonyms: "HidingPlace", description: "A room that requires a special trick to enter")
#
# @param [String,nil] label Optional label. When nil, infer the label from the tag name,
# converting `CamelCase` to `Camel Case`
# @param [String] synonyms A comma separated list of synonyms for this tag.
# @param [String] description A longer description of the tag.
# @param [kwargs] **tags Exactly one pair of `tag` => `parent`
# @return [Semantics::Tag] The created semantic tag class
#
def self.create(label: nil, synonyms: "", description: "", **tags)
raise "Tags must be specified" if tags.empty?
if (tags.length > 1) && !(label.nil? && synonyms.empty? && description.empty?)
raise "Additional options can only be specified when creating one tag"
end

tags.each do |name, parent|
CustomSemantic.create(name, parent, label: label, synonyms: synonyms, description: description)
end
end
end
end
end
end

0 comments on commit 429afbd

Please sign in to comment.