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 Apr 1, 2023
1 parent 0db2fef commit a17516f
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 4 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 adding custom semantic tags with {OpenHAB::Core::Items::Semantics.add Semantics.add}

### Bug Fixes

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

Background:
Given Clean OpenHAB with latest Ruby Libraries

Scenario: Semantic tag added
Given a rule
"""
locale = java.util.Locale.default
Semantics.add(<tag>: Semantics::<parent>)
member_of_tag = Semantics::<tag>.java_class < org.openhab.core.semantics.Tag.java_class
found_in_aggregator = org.openhab.core.semantics.model.<type>.<aggregator>.stream.any_match { |l| l == Semantics::<tag>.java_class }
found_by_id = org.openhab.core.semantics.SemanticTags.get_by_id("<tag>") == Semantics::<tag>.java_class
found_by_label = org.openhab.core.semantics.SemanticTags.get_by_label("<default_label>", locale) == Semantics::<tag>.java_class
logger.info "#{Semantics::<tag>.java_class}: #{member_of_tag} #{found_in_aggregator} #{found_by_id} #{found_by_label}"
"""
When I deploy the rule
Then It should log 'org.openhab.core.semantics.model.<type>.<tag>: true true true true' within 5 seconds
Examples:
| tag | parent | type | aggregator | default_label |
| SecretRoom2 | Room | location | Locations | Secret Room 2 |
| Equipment9 | Lightbulb | equipment | Equipments | Equipment 9 |
| Pointier | Control | point | Points | Pointier |
| Property8 | Property | property | Properties | Property 8 |

Scenario: Supports custom label
Given a rule
"""
Semantics.add(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, it doesn't load the synonyms other than from the resource bundle
# @wip
# Scenario: Support synonyms
# Given a rule
# """
# Semantics.add(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.add(<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 |
| Color | Light | point | color | LightColor | true |
| TorpedoLauncher | Equipment | equipment | contact | Launcher1 | true |
18 changes: 14 additions & 4 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 {add}
#
module Semantics
GenericItem.include(self)
Expand Down Expand Up @@ -188,11 +195,14 @@ module Semantics
# end
#

# @!visibility private
TAG_DB = [org.openhab.core.semantics.model.point.Points,
org.openhab.core.semantics.model.property.Properties,
org.openhab.core.semantics.model.equipment.Equipments,
org.openhab.core.semantics.model.location.Locations].freeze

# import all the semantics constants
[org.openhab.core.semantics.model.point.Points,
org.openhab.core.semantics.model.property.Properties,
org.openhab.core.semantics.model.equipment.Equipments,
org.openhab.core.semantics.model.location.Locations].each do |parent_tag|
TAG_DB.each do |parent_tag|
parent_tag.stream.for_each do |tag|
const_set(tag.simple_name.to_sym, tag.ruby_class)
end
Expand Down
167 changes: 167 additions & 0 deletions lib/openhab/core/items/semantics/custom_semantic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# 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 add custom Semantic tags
#
# @!visibility private
class CustomSemantic
class << self
#
# Adds 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,nil] The added semantic tag class,
# or nil if the tag had already been registered.
#
def add(name, parent, label: nil, synonyms: "", description: "")
name = name.to_s

return if name == self.class.name

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

class_loader = org.openhab.core.semantics.SemanticTags.java_class.class_loader

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}"

return unless class_loader.find_class(nil, class_name).nil?

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

# CamelCaseALL99 -> Camel Case ALL 99
label ||= name.gsub(/(([A-Z][a-z]+)|([A-Z][A-Z]+)|([0-9]+))/, " \\1").strip

# 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|
# Correct a bug in openhab, Semantics::Property's id is `MeasurementProperty` instead of Property
parent_id = "Property" if parent_id == "MeasurementProperty" # @deprecated OH3.4

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 = class_loader.define_class(class_name, byte_code, 0, byte_code.length)

register_tag(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
end
end

#
# Adds custom semantic tags.
#
# @return [Semantics::Tag] The added semantic tag class
#
# @overload self.add(**tags)
# Quickly add one or more semantic tags using the default label, empty synonyms and descriptions.
#
# @param [kwargs] **tags One or more `tag` => `parent` pairs
# @return [Boolean] true if all tags were added successfully
#
# @example Add one semantic tag `Balcony` whose parent is `Semantics::Outdoor` (Location)
# Semantics.add(Balcony: Semantics::Outdoor)
#
# @example Add multiple semantic tags
# Semantics.add(Balcony: Semantics::Outdoor,
# SecretRoom: Semantics::Room,
# Motion: Semantics::Property)
#
# @overload self.add(label: nil, synonyms: "", description: "", **tags)
# Add a custom semantic tag with extra details.
#
# @example
# Semantics.add(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 [Boolean] true if the tag was added successfully
#
def self.add(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.map do |name, parent|
CustomSemantic.add(name, parent, label: label, synonyms: synonyms, description: description)
end.any?(&:!)
end

#
# Automatically looks up new semantic classes and adds them as `constants`
#
# @return [Tag, nil]
#
def self.const_missing(sym)
logger.warn("const missing, performing Semantics Lookup for: #{sym}")
TAG_DB.lazy.flat_map { |tags| tags.stream.collect(java.util.stream.Collectors.to_list) }
.find { |tag| tag.simple_name.to_sym == sym }
&.then { |tag| const_set(tag.simple_name.to_sym, tag.ruby_class) }
end
end
end
end
end
8 changes: 8 additions & 0 deletions spec/openhab/core/items/semantics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,12 @@ def points(*args)
expect(gIndoor.equipments).to match_array([Group_Equipment, NonGroup_Equipment])
end
end

describe "#add" do
it "supports creating multiple tags" do
allow(Semantics::CustomSemantic).to receive(:add).and_return(true)
expect(Semantics::CustomSemantic).to receive(:add).twice
Semantics.add(Room1: Semantics::Room, Property2: Semantics::Property)
end
end
end

0 comments on commit a17516f

Please sign in to comment.